();
+ const encoder = new TextEncoder();
+
+ for (const [id, editor] of this.editors) {
+ const txt = editor.getModel()?.getValue();
+ const data = encoder.encode(txt);
+ const digest = await window.crypto.subtle.digest('SHA-1', data);
+ const hashArray = Array.from(new Uint8Array(digest));
+ const hash = hashArray
+ .map((b) => b.toString(16).padStart(2, '0'))
+ .join('');
+ hashes.set(id, hash);
+ }
+
+ for (const [id, backup] of this.backups) {
+ const txt = backup.model.getValue();
+ const data = encoder.encode(txt);
+ const digest = await window.crypto.subtle.digest('SHA-1', data);
+ const hashArray = Array.from(new Uint8Array(digest));
+ const hash = hashArray
+ .map((b) => b.toString(16).padStart(2, '0'))
+ .join('');
+ hashes.set(id, hash);
+ }
+
+ return hashes;
}
- private observeEdits(editor: Editor) {
- const disposable = editor.onDidChangeModelContent(() => {
- this.isEdited ||= true;
- disposable.dispose();
+ /**
+ * Marks the current state of all editors as saved.
+ */
+ public async markAsSaved() {
+ const hashes = await this.getAllHashes();
+ runInAction(() => {
+ this.savedHashes = hashes;
+ // new map to clone
+ this.currentHashes = new Map(hashes);
});
}
+ /**
+ * Forces all editors to be marked as unsaved.
+ */
+ public clearSaved() {
+ this.savedHashes.clear();
+ }
public editorSeverityMap = observable.map<
EditorId,
MonacoType.MarkerSeverity
diff --git a/src/renderer/file-manager.ts b/src/renderer/file-manager.ts
index bca106fab0..b03a236307 100644
--- a/src/renderer/file-manager.ts
+++ b/src/renderer/file-manager.ts
@@ -38,17 +38,20 @@ export class FileManager {
},
);
- window.ElectronFiddle.addEventListener('saved-local-fiddle', (filePath) => {
- const { localPath } = this.appState;
+ window.ElectronFiddle.addEventListener(
+ 'saved-local-fiddle',
+ async (filePath) => {
+ const { localPath } = this.appState;
- if (filePath !== localPath) {
- this.appState.localPath = filePath;
- this.appState.gistId = undefined;
- }
- window.ElectronFiddle.setShowMeTemplate();
- this.appState.templateName = undefined;
- this.appState.editorMosaic.isEdited = false;
- });
+ if (filePath !== localPath) {
+ this.appState.localPath = filePath;
+ this.appState.gistId = undefined;
+ }
+ window.ElectronFiddle.setShowMeTemplate();
+ this.appState.templateName = undefined;
+ await this.appState.editorMosaic.markAsSaved();
+ },
+ );
window.ElectronFiddle.onGetFiles(this.getFiles);
}
diff --git a/src/renderer/state.ts b/src/renderer/state.ts
index 20d0536e17..c421af435f 100644
--- a/src/renderer/state.ts
+++ b/src/renderer/state.ts
@@ -565,9 +565,9 @@ export class AppState {
* @returns the title, e.g. appname, fiddle name, state
*/
get title(): string {
- const { isEdited } = this.editorMosaic;
-
- return isEdited ? 'Electron Fiddle - Unsaved' : 'Electron Fiddle';
+ return this.editorMosaic.isEdited
+ ? 'Electron Fiddle - Unsaved'
+ : 'Electron Fiddle';
}
/**
diff --git a/tests/mocks/monaco.ts b/tests/mocks/monaco.ts
index 7bb8e216f4..f06d4732c3 100644
--- a/tests/mocks/monaco.ts
+++ b/tests/mocks/monaco.ts
@@ -75,6 +75,7 @@ export class MonacoEditorMock {
private model = new MonacoModelMock('', 'javascript');
private scrollHeight = 0;
+ public focus = vi.fn();
public addCommand = vi.fn();
public dispose = vi.fn();
public getAction = vi.fn(() => this.action);
diff --git a/tests/renderer/app-spec.tsx b/tests/renderer/app-spec.tsx
index 530f39ecd5..ea96ce6af1 100644
--- a/tests/renderer/app-spec.tsx
+++ b/tests/renderer/app-spec.tsx
@@ -3,7 +3,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { EditorValues, MAIN_JS, SetFiddleOptions } from '../../src/interfaces';
import { App } from '../../src/renderer/app';
-import { EditorMosaic, EditorPresence } from '../../src/renderer/editor-mosaic';
+import { EditorPresence } from '../../src/renderer/editor-mosaic';
import { defaultDark, defaultLight } from '../../src/themes-defaults';
import { createEditorValues } from '../mocks/mocks';
@@ -26,7 +26,7 @@ describe('App component', () => {
document.body.innerHTML = '';
});
- beforeEach(() => {
+ beforeEach(async () => {
vi.mocked(window.ElectronFiddle.getTemplate).mockResolvedValue({
[MAIN_JS]: '// content',
});
@@ -51,7 +51,7 @@ describe('App component', () => {
});
window.app = app;
- state.editorMosaic.set({ [MAIN_JS]: '// content' });
+ await state.editorMosaic.set({ [MAIN_JS]: '// content' });
state.editorMosaic.files.set(MAIN_JS, EditorPresence.Pending);
});
@@ -174,8 +174,10 @@ describe('App component', () => {
expect(app.state.gistId).toBeFalsy();
expect(app.state.localPath).toBe(localPath);
- // ...mark it as edited so a confirm dialog will appear before replacing
- app.state.editorMosaic.isEdited = true;
+ vi.spyOn(app.state.editorMosaic, 'isEdited', 'get').mockReturnValue(
+ true,
+ );
+
app.state.showConfirmDialog = vi.fn().mockResolvedValue(confirm);
// now try to replace
@@ -386,11 +388,6 @@ describe('App component', () => {
// make a second fiddle that differs from the first
const editorValues = createEditorValues();
const editorValues2: EditorValues = { [MAIN_JS]: '// hello world' };
- let editorMosaic: EditorMosaic;
-
- beforeEach(() => {
- ({ editorMosaic } = app.state);
- });
async function testDialog(confirm: boolean) {
const localPath = '/etc/passwd';
@@ -405,7 +402,7 @@ describe('App component', () => {
// ...mark it as edited so that trying a confirm dialog
// will be triggered when we try to replace it
- editorMosaic.isEdited = true;
+ vi.spyOn(app.state.editorMosaic, 'isEdited', 'get').mockReturnValue(true);
// set up a reaction to confirm the replacement
// when it happens
@@ -440,13 +437,18 @@ describe('App component', () => {
it('can close the window if user accepts the dialog', async () => {
app.state.showConfirmDialog = vi.fn().mockResolvedValueOnce(true);
- // expect the app to be watching for exit if the fiddle is edited
- app.state.editorMosaic.isEdited = true;
- expect(window.onbeforeunload).toBeTruthy();
+ // Actually set the editor in a dirty state instead of mocking
+ // because this code path uses an autorun on the computed `isEdited` value
+ (app.state.editorMosaic as any).savedHashes = new Map([[MAIN_JS, 'def']]);
+ (app.state.editorMosaic as any).currentHashes = new Map([
+ [MAIN_JS, 'abc'],
+ ]);
const e = {
returnValue: Boolean,
};
+
+ await vi.waitUntil(() => app.state.editorMosaic.isEdited === true);
window.onbeforeunload!(e as any);
expect(e.returnValue).toBe(false);
@@ -460,8 +462,13 @@ describe('App component', () => {
app.state.isQuitting = true;
app.state.showConfirmDialog = vi.fn().mockResolvedValueOnce(true);
- // expect the app to be watching for exit if the fiddle is edited
- app.state.editorMosaic.isEdited = true;
+ // Actually set the editor in a dirty state instead of mocking
+ // because this code path uses an autorun on the computed `isEdited` value
+ (app.state.editorMosaic as any).savedHashes = new Map([[MAIN_JS, 'def']]);
+ (app.state.editorMosaic as any).currentHashes = new Map([
+ [MAIN_JS, 'abc'],
+ ]);
+
expect(window.onbeforeunload).toBeTruthy();
const e = {
@@ -482,7 +489,9 @@ describe('App component', () => {
app.state.showConfirmDialog = vi.fn().mockResolvedValueOnce(false);
// expect the app to be watching for exit if the fiddle is edited
- app.state.editorMosaic.isEdited = true;
+ (app.state.editorMosaic as any) = vi.fn(() => ({
+ isEdited: true,
+ }))();
expect(window.onbeforeunload).toBeTruthy();
const e = {
diff --git a/tests/renderer/components/__snapshots__/sidebar-file-tree-spec.tsx.snap b/tests/renderer/components/__snapshots__/sidebar-file-tree-spec.tsx.snap
deleted file mode 100644
index 2c8f5025c7..0000000000
--- a/tests/renderer/components/__snapshots__/sidebar-file-tree-spec.tsx.snap
+++ /dev/null
@@ -1,1003 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`SidebarFileTree component > can bring up the Add File input 1`] = `
-
-
-
-
-
- }
- onClick={[Function]}
- >
- index.html
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 1,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- main.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 2,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- preload.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 3,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- renderer.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 4,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- styles.css
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "className": "add-file-input",
- "icon": "document",
- "id": "add",
- "label": ,
- },
- ],
- "hasCaret": false,
- "icon": "folder-open",
- "id": "files",
- "isExpanded": true,
- "label": "Editors",
- "secondaryLabel":
-
-
-
-
-
-
- ,
- },
- ]
- }
- />
-
-`;
-
-exports[`SidebarFileTree component > reflects the visibility state of all icons 1`] = `
-
-
-
-
-
- }
- onClick={[Function]}
- >
- index.html
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 1,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- main.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 2,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- preload.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 3,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- renderer.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 4,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- styles.css
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- ],
- "hasCaret": false,
- "icon": "folder-open",
- "id": "files",
- "isExpanded": true,
- "label": "Editors",
- "secondaryLabel":
-
-
-
-
-
-
- ,
- },
- ]
- }
- />
-
-`;
-
-exports[`SidebarFileTree component > renders 1`] = `
-
-
-
-
-
- }
- onClick={[Function]}
- >
- index.html
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 1,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- main.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 2,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- preload.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 3,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- renderer.js
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- {
- "hasCaret": false,
- "icon": "document",
- "id": 4,
- "isSelected": false,
- "label":
-
-
-
- }
- onClick={[Function]}
- >
- styles.css
- ,
- "secondaryLabel":
-
-
-
-
-
- ,
- },
- ],
- "hasCaret": false,
- "icon": "folder-open",
- "id": "files",
- "isExpanded": true,
- "label": "Editors",
- "secondaryLabel":
-
-
-
-
-
-
- ,
- },
- ]
- }
- />
-
-`;
diff --git a/tests/renderer/components/sidebar-file-tree-spec.tsx b/tests/renderer/components/sidebar-file-tree-spec.tsx
deleted file mode 100644
index 5a45f20144..0000000000
--- a/tests/renderer/components/sidebar-file-tree-spec.tsx
+++ /dev/null
@@ -1,251 +0,0 @@
-import * as React from 'react';
-
-import { shallow } from 'enzyme';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-
-import {
- EditorValues,
- MAIN_CJS,
- MAIN_JS,
- PACKAGE_NAME,
-} from '../../../src/interfaces';
-import { Editors } from '../../../src/renderer/components/editors';
-import { SidebarFileTree } from '../../../src/renderer/components/sidebar-file-tree';
-import {
- EditorMosaic,
- EditorPresence,
-} from '../../../src/renderer/editor-mosaic';
-import { AppState } from '../../../src/renderer/state';
-import { createEditorValues } from '../../mocks/editor-values';
-import { AppMock, StateMock } from '../../mocks/mocks';
-
-describe('SidebarFileTree component', () => {
- let store: AppState;
- let editorMosaic: EditorMosaic;
- let editorValues: EditorValues;
- let stateMock: StateMock;
-
- beforeEach(() => {
- ({ state: stateMock } = window.app as unknown as AppMock);
- store = {} as unknown as AppState;
- editorValues = createEditorValues();
- editorMosaic = new EditorMosaic();
- editorMosaic.set(editorValues);
- (store as unknown as StateMock).editorMosaic = editorMosaic;
- stateMock.editorMosaic = editorMosaic;
- });
-
- afterEach(() => {
- vi.useRealTimers();
- });
-
- it('renders', () => {
- const wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
- });
-
- it('reflects the visibility state of all icons', () => {
- editorMosaic.hide('index.html');
- const wrapper = shallow();
-
- // snapshot has an 'eye-off' icon
- expect(wrapper).toMatchSnapshot();
- });
-
- it('can bring up the Add File input', () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- instance.setState({ action: 'add' });
-
- // snapshot has the input rendered
- expect(wrapper).toMatchSnapshot();
- });
-
- it('can toggle editor visibility', () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- instance.toggleVisibility('index.html');
-
- expect(editorMosaic.files.get('index.html')).toBe(EditorPresence.Hidden);
- });
-
- it('can create new editors', () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- expect(editorMosaic.files.get('tester.js')).toBe(undefined);
- instance.createEditor('tester.js');
- expect(editorMosaic.files.get('tester.js')).toBe(EditorPresence.Pending);
- });
-
- it('can delete editors', () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- expect(editorMosaic.files.get('index.html')).toBe(EditorPresence.Pending);
- instance.removeEditor('index.html');
- expect(editorMosaic.files.get('index.html')).toBe(undefined);
- });
-
- it('can rename editors', async () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- const EDITOR_NAME = 'index.html';
- const EDITOR_NEW_NAME = 'new_index.html';
-
- store.showInputDialog = vi.fn().mockResolvedValueOnce(EDITOR_NEW_NAME);
-
- expect(editorMosaic.files.get(EDITOR_NAME)).toBe(EditorPresence.Pending);
-
- await instance.renameEditor(EDITOR_NAME);
-
- expect(editorMosaic.files.get(EDITOR_NAME)).toBe(undefined);
- expect(editorMosaic.files.get(EDITOR_NEW_NAME)).toBe(
- EditorPresence.Pending,
- );
- });
-
- it('can rename one main entry point file to another main entry point file', async () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- const EDITOR_NAME = MAIN_JS;
- const EDITOR_NEW_NAME = MAIN_CJS;
-
- store.showInputDialog = vi.fn().mockResolvedValueOnce(EDITOR_NEW_NAME);
-
- await instance.renameEditor(EDITOR_NAME);
-
- expect(editorMosaic.files.get(EDITOR_NAME)).toBe(undefined);
- expect(editorMosaic.files.get(EDITOR_NEW_NAME)).toBe(
- EditorPresence.Pending,
- );
- });
-
- it('fails if trying to rename an editor to package(-lock).json', async () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- const EDITOR_NAME = 'index.html';
- const EDITOR_NEW_NAME = PACKAGE_NAME;
-
- store.showInputDialog = vi.fn().mockResolvedValueOnce(EDITOR_NEW_NAME);
- store.showErrorDialog = vi.fn().mockResolvedValueOnce(true);
-
- await instance.renameEditor(EDITOR_NAME);
-
- expect(store.showErrorDialog).toHaveBeenCalledWith(
- `Cannot add ${PACKAGE_NAME} or package-lock.json as custom files`,
- );
- expect(editorMosaic.files.get(EDITOR_NAME)).toBe(EditorPresence.Pending);
- });
-
- it('fails if trying to rename an editor to an unsupported name', async () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- const EDITOR_NAME = 'index.html';
- const EDITOR_NEW_NAME = 'data.txt';
-
- store.showInputDialog = vi.fn().mockResolvedValueOnce(EDITOR_NEW_NAME);
- store.showErrorDialog = vi.fn().mockResolvedValueOnce(true);
-
- await instance.renameEditor(EDITOR_NAME);
-
- expect(store.showErrorDialog).toHaveBeenCalledWith(
- `Invalid filename "${EDITOR_NEW_NAME}": Must be a file ending in .cjs, .js, .mjs, .html, .css, or .json`,
- );
- expect(editorMosaic.files.get(EDITOR_NAME)).toBe(EditorPresence.Pending);
- });
-
- it('fails if trying to rename an editor to an existing name', async () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- const EXISTED_NAME = 'styles.css';
- const TO_BE_NAMED = 'index.html';
- const EDITOR_NEW_NAME = EXISTED_NAME;
-
- store.showInputDialog = vi.fn().mockResolvedValueOnce(EDITOR_NEW_NAME);
- store.showErrorDialog = vi.fn().mockResolvedValueOnce(true);
-
- await instance.renameEditor(TO_BE_NAMED);
-
- expect(store.showErrorDialog).toHaveBeenCalledWith(
- `Cannot rename file to "${EDITOR_NEW_NAME}": File already exists`,
- );
- expect(editorMosaic.files.get(TO_BE_NAMED)).toBe(EditorPresence.Pending);
- });
-
- it('fails if trying to rename an editor to another main entry point file', async () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- const TO_BE_NAMED = 'index.html';
- const EDITOR_NEW_NAME = MAIN_CJS;
-
- store.showInputDialog = vi.fn().mockResolvedValueOnce(EDITOR_NEW_NAME);
- store.showErrorDialog = vi.fn().mockResolvedValueOnce(true);
-
- await instance.renameEditor(TO_BE_NAMED);
-
- expect(store.showErrorDialog).toHaveBeenCalledWith(
- `Cannot rename file to "${EDITOR_NEW_NAME}": Main entry point ${MAIN_JS} exists`,
- );
- expect(editorMosaic.files.get(TO_BE_NAMED)).toBe(EditorPresence.Pending);
- });
-
- it('can reset the editor layout', () => {
- const wrapper = shallow();
- const instance: any = wrapper.instance();
-
- editorMosaic.resetLayout = vi.fn();
-
- instance.resetLayout();
-
- expect(editorMosaic.resetLayout).toHaveBeenCalledTimes(1);
- });
-
- it('file is visible, click files tree, focus file content', async () => {
- vi.useFakeTimers();
-
- const sidebarFileTree = shallow();
- const editors = shallow(
- ,
- );
- const sidebarFileTreeInstance: any = sidebarFileTree.instance();
- const editorsInstance: any = editors.instance();
-
- sidebarFileTreeInstance.setFocusedFile('index.html');
-
- setTimeout(() => {
- expect(editorMosaic.files.get('index.html')).toBe(EditorPresence.Visible);
- expect(editorsInstance.state.focused).toBe('index.html');
- });
- });
-
- it('file is hidden, click files tree, make file visible and focus file content', async () => {
- vi.useFakeTimers();
-
- const sidebarFileTree = shallow();
- const editors = shallow(
- ,
- );
- const sidebarFileTreeInstance: any = sidebarFileTree.instance();
- const editorsInstance: any = editors.instance();
-
- sidebarFileTreeInstance.toggleVisibility('index.html');
-
- expect(editorMosaic.files.get('index.html')).toBe(EditorPresence.Hidden);
-
- sidebarFileTreeInstance.setFocusedFile('index.html');
-
- setTimeout(() => {
- expect(editorMosaic.files.get('index.html')).toBe(EditorPresence.Visible);
- expect(editorsInstance.state.focused).toBe('index.html');
- });
- });
-});
diff --git a/tests/renderer/editor-mosaic-spec.ts b/tests/renderer/editor-mosaic-spec.ts
index 5ac191b889..5351a11d8c 100644
--- a/tests/renderer/editor-mosaic-spec.ts
+++ b/tests/renderer/editor-mosaic-spec.ts
@@ -1,4 +1,3 @@
-import { reaction } from 'mobx';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { EditorId, EditorValues, MAIN_JS } from '../../src/interfaces';
@@ -37,67 +36,43 @@ describe('EditorMosaic', () => {
const id = MAIN_JS;
const content = '// content';
- beforeEach(() => {
- editorMosaic.set({ [id]: content });
+ beforeEach(async () => {
+ await editorMosaic.set({ [id]: content });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Pending);
});
- it('throws when called on an unexpected file', () => {
+ it('throws when called on an unexpected file', async () => {
const otherId = 'file.js';
- expect(() => editorMosaic.addEditor(otherId, editor)).toThrow(
- /unexpected file/i,
- );
+ await expect(() =>
+ editorMosaic.addEditor(otherId, editor),
+ ).rejects.toThrow(/unexpected file/i);
});
- it('makes a file visible', () => {
- editorMosaic.addEditor(id, editor);
+ it('makes a file visible', async () => {
+ await editorMosaic.addEditor(id, editor);
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Visible);
});
- it('begins listening for changes to the files', () => {
- // test that isEdited is not affected by editors that aren't in the
- // mosaic (`editor` hasn't been added to the mosaic yet)
- expect(editorMosaic.isEdited).toBe(false);
- editor.setValue('💩');
- expect(editorMosaic.isEdited).toBe(false);
-
- // test that isEdited is affected by editors that have been added
- editorMosaic.addEditor(id, editor);
- expect(editorMosaic.isEdited).toBe(false);
- editor.setValue('💩');
- expect(editorMosaic.isEdited).toBe(true);
- });
-
- describe('does not change isEdited', () => {
- it.each([true, false])('...to %p', (value: boolean) => {
- // test that isEdited does not change when adding editors
- editorMosaic.isEdited = value;
- expect(editorMosaic.isEdited).toBe(value);
- editorMosaic.addEditor(id, editor);
- expect(editorMosaic.isEdited).toBe(value);
- });
- });
-
- it('restores ViewStates when possible', () => {
+ it('restores ViewStates when possible', async () => {
// setup: put visible file into the mosaic and then hide it.
// this should cause EditorMosaic to cache the viewstate offscreen.
const viewState = Symbol('some unique viewstate');
vi.mocked(editor.saveViewState).mockReturnValueOnce(viewState as any);
- editorMosaic.addEditor(id, editor);
+ await editorMosaic.addEditor(id, editor);
editorMosaic.hide(id);
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
// now try to re-show the file
const editor2: Editor = new MonacoEditorMock() as unknown as Editor;
editorMosaic.show(id);
- editorMosaic.addEditor(id, editor2);
+ await editorMosaic.addEditor(id, editor2);
// test that the viewState was reused in the new editor
expect(editor2.restoreViewState).toHaveBeenCalledWith(viewState);
});
- it('restores values when possible', () => {
- editorMosaic.addEditor(id, editor);
+ it('restores values when possible', async () => {
+ await editorMosaic.addEditor(id, editor);
expect(monaco.editor.createModel).toHaveBeenCalledWith(
content,
expect.anything(),
@@ -111,31 +86,31 @@ describe('EditorMosaic', () => {
const hiddenContent = getEmptyContent(id);
const visibleContent = '// fnord' as const;
- it('excludes hidden files', () => {
- editorMosaic.set({ [id]: hiddenContent });
+ it('excludes hidden files', async () => {
+ await editorMosaic.set({ [id]: hiddenContent });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
expect(editorMosaic.numVisible).toBe(0);
});
- it('includes pending files', () => {
- editorMosaic.set({ [id]: visibleContent });
+ it('includes pending files', async () => {
+ await editorMosaic.set({ [id]: visibleContent });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Pending);
expect(editorMosaic.numVisible).toBe(1);
});
- it('includes visible files', () => {
- editorMosaic.set({ [id]: visibleContent });
- editorMosaic.addEditor(id, editor);
+ it('includes visible files', async () => {
+ await editorMosaic.set({ [id]: visibleContent });
+ await editorMosaic.addEditor(id, editor);
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Visible);
expect(editorMosaic.numVisible).toBe(1);
});
});
describe('hide()', () => {
- it('hides an editor', () => {
+ it('hides an editor', async () => {
const id = MAIN_JS;
- editorMosaic.set(valuesIn);
- editorMosaic.addEditor(id, editor);
+ await editorMosaic.set(valuesIn);
+ await editorMosaic.addEditor(id, editor);
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Visible);
editorMosaic.hide(MAIN_JS);
@@ -144,9 +119,9 @@ describe('EditorMosaic', () => {
});
describe('show()', () => {
- it('shows an editor', () => {
+ it('shows an editor', async () => {
const id = MAIN_JS;
- editorMosaic.set({ [id]: getEmptyContent(id) });
+ await editorMosaic.set({ [id]: getEmptyContent(id) });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
editorMosaic.show(MAIN_JS);
@@ -159,25 +134,25 @@ describe('EditorMosaic', () => {
const hiddenContent = getEmptyContent(id);
const visibleContent = '// sesquipedalian';
- it('shows files that were hidden', () => {
- editorMosaic.set({ [id]: hiddenContent });
+ it('shows files that were hidden', async () => {
+ await editorMosaic.set({ [id]: hiddenContent });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
editorMosaic.toggle(id);
expect(editorMosaic.files.get(id)).not.toBe(EditorPresence.Hidden);
});
- it('hides files that were visible', () => {
- editorMosaic.set({ [id]: visibleContent });
- editorMosaic.addEditor(id, editor);
+ it('hides files that were visible', async () => {
+ await editorMosaic.set({ [id]: visibleContent });
+ await editorMosaic.addEditor(id, editor);
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Visible);
editorMosaic.toggle(id);
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
});
- it('hides files that were pending', () => {
- editorMosaic.set({ [id]: visibleContent });
+ it('hides files that were pending', async () => {
+ await editorMosaic.set({ [id]: visibleContent });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Pending);
editorMosaic.toggle(id);
@@ -186,34 +161,35 @@ describe('EditorMosaic', () => {
});
describe('resetLayout()', () => {
- it('resets editors to their original arrangement', () => {
+ it('resets editors to their original arrangement', async () => {
const serializeState = () => [...editorMosaic.files.entries()];
// setup: capture the state of the editorMosaic after set() is called
- editorMosaic.set(valuesIn);
+ await editorMosaic.set(valuesIn);
const initialState = serializeState();
// now change the state a bit
- for (const filename of Object.keys(valuesIn))
+ for (const filename of Object.keys(valuesIn)) {
editorMosaic.hide(filename as EditorId);
+ }
expect(serializeState()).not.toStrictEqual(initialState);
// test the post-reset state matches the initial state
- editorMosaic.resetLayout();
+ await editorMosaic.resetLayout();
expect(serializeState()).toStrictEqual(initialState);
});
});
describe('values()', () => {
- it('works on closed panels', () => {
+ it('works on closed panels', async () => {
const values = createEditorValues();
- editorMosaic.set(values);
+ await editorMosaic.set(values);
expect(editorMosaic.values()).toStrictEqual(values);
});
- it('works on open panels', () => {
+ it('works on open panels', async () => {
const values = createEditorValues();
- editorMosaic.set(values);
+ await editorMosaic.set(values);
// now modify values _after_ calling editorMosaic.set()
for (const [file, value] of Object.entries(values)) {
@@ -223,8 +199,8 @@ describe('EditorMosaic', () => {
// and then add Monaco editors
for (const [file, value] of Object.entries(values)) {
const editor = new MonacoEditorMock() as unknown as Editor;
- editorMosaic.addEditor(file as EditorId, editor);
- editor.setValue(value as string);
+ await editorMosaic.addEditor(file as EditorId, editor);
+ editor.setValue(value);
}
// values() should match the modified values
@@ -233,81 +209,87 @@ describe('EditorMosaic', () => {
});
describe('addNewFile()', () => {
- it('sets isEdited to true', () => {
- editorMosaic.set(createEditorValues());
- editorMosaic.isEdited = false;
- editorMosaic.addNewFile('foo.js');
+ it('sets isEdited to true', async () => {
+ await editorMosaic.set(createEditorValues());
+ await editorMosaic.markAsSaved();
+ expect(editorMosaic.isEdited).toBe(false);
+ await editorMosaic.addNewFile('foo.js');
expect(editorMosaic.isEdited).toBe(true);
});
});
describe('renameFile()', () => {
- it('sets isEdited to true', () => {
- editorMosaic.set(createEditorValues());
- editorMosaic.isEdited = false;
- editorMosaic.renameFile('renderer.js', 'bar.js');
+ it('sets isEdited to true', async () => {
+ await editorMosaic.set(createEditorValues());
+ await editorMosaic.markAsSaved();
+ expect(editorMosaic.isEdited).toBe(false);
+ await editorMosaic.renameFile('renderer.js', 'bar.js');
expect(editorMosaic.isEdited).toBe(true);
});
});
describe('remove()', () => {
- it('sets isEdited to true', () => {
- editorMosaic.set(createEditorValues());
- editorMosaic.isEdited = false;
- editorMosaic.remove('renderer.js');
+ it('sets isEdited to true', async () => {
+ await editorMosaic.set(createEditorValues());
+ await editorMosaic.markAsSaved();
+ expect(editorMosaic.isEdited).toBe(false);
+ await editorMosaic.remove('renderer.js');
expect(editorMosaic.isEdited).toBe(true);
});
});
describe('set()', () => {
- it('resets isEdited to false', () => {
- editorMosaic.isEdited = true;
- editorMosaic.set(createEditorValues());
+ it('resets isEdited to false', async () => {
+ await editorMosaic.set(valuesIn);
+ editor.setValue('beep boop');
+ vi.waitFor(() => editorMosaic.isEdited === true);
+
+ await editorMosaic.set(createEditorValues());
expect(editorMosaic.isEdited).toBe(false);
});
- it('hides files that are empty', () => {
+ it('hides files that are empty', async () => {
const id = MAIN_JS;
const content = '';
- editorMosaic.set({ [id]: content });
+ await editorMosaic.set({ [id]: content });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
});
- it('hides files that have default content', () => {
+ it('hides files that have default content', async () => {
const id = MAIN_JS;
const content = getEmptyContent(id);
expect(content).not.toStrictEqual('');
- editorMosaic.set({ [id]: content });
+ await editorMosaic.set({ [id]: content });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
});
- it('shows files that have non-default content', () => {
+ it('shows files that have non-default content', async () => {
const id = MAIN_JS;
const content = 'fnord';
- editorMosaic.set({ [id]: content });
+ await editorMosaic.set({ [id]: content });
expect(editorMosaic.files.get(id)).not.toBe(EditorPresence.Hidden);
});
- it('does not set a value if none passed in', () => {
+ it('does not set a value if none passed in', async () => {
const id = MAIN_JS;
- editorMosaic.set({ [id]: '// content' });
+ await editorMosaic.set({ [id]: '// content' });
expect(editorMosaic.files.has('some-file.js')).toBe(false);
});
describe('reuses existing editors', () => {
- it('when the old file was visible and the new one should be too', () => {
+ it('when the old file was visible and the new one should be too', async () => {
// setup: get a mosaic with a visible editor
const id = MAIN_JS;
let content = '// first content';
- editorMosaic.set({ [id]: content });
- editorMosaic.addEditor(id, editor);
+ await editorMosaic.set({ [id]: content });
+ await editorMosaic.addEditor(id, editor);
// now call set again, same filename DIFFERENT content
content = '// second content';
vi.mocked(monaco.editor.getModel).mockReturnValueOnce(
monaco.latestModel,
);
- editorMosaic.set({ [id]: content });
+ await editorMosaic.set({ [id]: content });
// test that editorMosaic set the editor to the new content
expect(editor.getValue()).toBe(content);
expect(editorMosaic.isEdited).toBe(false);
@@ -318,10 +300,10 @@ describe('EditorMosaic', () => {
monaco.latestModel,
);
editor.setValue(content);
- expect(editorMosaic.isEdited).toBe(true);
+ vi.waitUntil(() => editorMosaic.isEdited === true);
// now call set again, same filename and SAME content
- editorMosaic.set({ [id]: content });
+ await editorMosaic.set({ [id]: content });
expect(editorMosaic.isEdited).toBe(false);
// test that the editor still responds to edits
@@ -330,15 +312,15 @@ describe('EditorMosaic', () => {
monaco.latestModel,
);
editor.setValue(content);
- expect(editorMosaic.isEdited).toBe(true);
+ vi.waitUntil(() => editorMosaic.isEdited === true);
});
- it('but not when the new file should be hidden', () => {
+ it('but not when the new file should be hidden', async () => {
// set up a fully populated mosaic with visible files
- editorMosaic.set(valuesIn);
+ await editorMosaic.set(valuesIn);
for (const [id, presence] of editorMosaic.files) {
if (presence === EditorPresence.Pending) {
- editorMosaic.addEditor(
+ await editorMosaic.addEditor(
id,
new MonacoEditorMock() as unknown as Editor,
);
@@ -349,7 +331,7 @@ describe('EditorMosaic', () => {
const keys = Object.keys(valuesIn);
const [id1, id2] = keys;
const values = { [id1]: '// potrzebie', [id2]: '' };
- editorMosaic.set(values);
+ await editorMosaic.set(values);
// test that id1 got recycled but id2 is hidden
const { files } = editorMosaic;
@@ -359,8 +341,8 @@ describe('EditorMosaic', () => {
});
});
- it('does not add unrequested files', () => {
- editorMosaic.set(valuesIn);
+ it('does not add unrequested files', async () => {
+ await editorMosaic.set(valuesIn);
for (const key of editorMosaic.files.keys()) {
expect(valuesIn).toHaveProperty([key]);
}
@@ -369,36 +351,36 @@ describe('EditorMosaic', () => {
describe('does not remember files from previous calls', () => {
const id = MAIN_JS;
- afterEach(() => {
+ afterEach(async () => {
// this is the real test.
// the three it()s below each set a different test condition
- editorMosaic.set({});
+ await editorMosaic.set({});
expect(editorMosaic.files.has(id)).toBe(false);
expect(editorMosaic.value(id)).toBe('');
});
- it('even if the file was visible', () => {
+ it('even if the file was visible', async () => {
const content = '// fnord';
- editorMosaic.set({ [id]: content });
- editorMosaic.addEditor(id, editor);
+ await editorMosaic.set({ [id]: content });
+ await editorMosaic.addEditor(id, editor);
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Visible);
});
- it('even if the file was hidden', () => {
+ it('even if the file was hidden', async () => {
const content = '';
- editorMosaic.set({ [id]: content });
+ await editorMosaic.set({ [id]: content });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
});
- it('even if the file was pending', () => {
+ it('even if the file was pending', async () => {
const content = '// fnord';
- editorMosaic.set({ [id]: content });
+ await editorMosaic.set({ [id]: content });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Pending);
});
});
- it('uses the expected layout', () => {
- editorMosaic.set(valuesIn);
+ it('uses the expected layout', async () => {
+ await editorMosaic.set(valuesIn);
expect(editorMosaic.mosaic).toStrictEqual({
direction: 'row',
first: {
@@ -424,26 +406,26 @@ describe('EditorMosaic', () => {
const content = '// content';
const emptyContent = getEmptyContent(id);
- it('returns values for files that are hidden', () => {
+ it('returns values for files that are hidden', async () => {
const value = emptyContent;
- editorMosaic.set({ [id]: value });
+ await editorMosaic.set({ [id]: value });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Hidden);
expect(editorMosaic.value(id)).toBe(value);
});
- it('returns values for files that are pending', () => {
+ it('returns values for files that are pending', async () => {
const value = content;
- editorMosaic.set({ [id]: value });
+ await editorMosaic.set({ [id]: value });
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Pending);
expect(editorMosaic.value(id)).toBe(value);
});
- it('returns values for files that are visible', () => {
+ it('returns values for files that are visible', async () => {
const value = content;
- editorMosaic.set({ [id]: value });
- editorMosaic.addEditor(id, editor);
+ await editorMosaic.set({ [id]: value });
+ await editorMosaic.addEditor(id, editor);
expect(editorMosaic.files.get(id)).toBe(EditorPresence.Visible);
expect(editorMosaic.value(id)).toBe(value);
@@ -455,17 +437,17 @@ describe('EditorMosaic', () => {
});
describe('getFocusedEditor', () => {
- it('finds the focused editor if there is one', () => {
+ it('finds the focused editor if there is one', async () => {
const id = MAIN_JS;
- editorMosaic.set(valuesIn);
- editorMosaic.addEditor(id, editor);
+ await editorMosaic.set(valuesIn);
+ await editorMosaic.addEditor(id, editor);
vi.mocked(editor.hasTextFocus).mockReturnValue(true);
expect(editorMosaic.getFocusedEditor()).toBe(editor);
});
- it('returns undefined if none have focus', () => {
- editorMosaic.set(valuesIn);
+ it('returns undefined if none have focus', async () => {
+ await editorMosaic.set(valuesIn);
expect(editorMosaic.getFocusedEditor()).toBeUndefined();
});
});
@@ -475,8 +457,8 @@ describe('EditorMosaic', () => {
const id = MAIN_JS;
const content = '// content';
const editor = new MonacoEditorMock() as unknown as Editor;
- editorMosaic.set({ [id]: content });
- editorMosaic.addEditor(id, editor);
+ await editorMosaic.set({ [id]: content });
+ await editorMosaic.addEditor(id, editor);
editorMosaic.layout();
editorMosaic.layout();
@@ -501,54 +483,21 @@ describe('EditorMosaic', () => {
const id = MAIN_JS;
let editor: Editor;
- beforeEach(() => {
+ beforeEach(async () => {
editor = new MonacoEditorMock() as unknown as Editor;
- editorMosaic.set(valuesIn);
- editorMosaic.addEditor(id, editor);
+ await editorMosaic.set(valuesIn);
+ await editorMosaic.addEditor(id, editor);
expect(editorMosaic.isEdited).toBe(false);
});
- function testForIsEdited() {
+ it('recognizes edits', async () => {
+ await editorMosaic.markAsSaved();
expect(editorMosaic.isEdited).toBe(false);
editor.setValue(`${editor.getValue()} more text`);
- expect(editorMosaic.isEdited).toBe(true);
- }
-
- it('recognizes edits', () => {
- testForIsEdited();
- });
-
- it('recognizes edits after isEdited has been manually set to false', () => {
- editorMosaic.isEdited = false;
- testForIsEdited();
- });
-
- it('recognizes edits after isEdited has been manually toggled', () => {
- editorMosaic.isEdited = true;
- editorMosaic.isEdited = false;
- testForIsEdited();
- });
-
- it('does not re-emit when isEdited is already true', () => {
- let changeCount = 0;
- const dispose = reaction(
- () => editorMosaic.isEdited,
- () => ++changeCount,
- );
- expect(editorMosaic.isEdited).toBe(false);
- expect(changeCount).toBe(0);
-
- editor.setValue(`${editor.getValue()} more text`);
- expect(editorMosaic.isEdited).toBe(true);
- expect(changeCount).toBe(1);
-
- editor.setValue(`${editor.getValue()} and even more text`);
- expect(editorMosaic.isEdited).toBe(true);
- expect(changeCount).toBe(1);
-
- dispose();
+ // hashes are calculated asynchronously
+ await vi.waitUntil(() => editorMosaic.isEdited === true);
});
});
diff --git a/tests/renderer/state-spec.ts b/tests/renderer/state-spec.ts
index 6e89845f20..d4bff37084 100644
--- a/tests/renderer/state-spec.ts
+++ b/tests/renderer/state-spec.ts
@@ -459,7 +459,7 @@ describe('AppState', () => {
it('if there is no current fiddle', async () => {
// setup: current fiddle is empty
- appState.editorMosaic.set({});
+ await appState.editorMosaic.set({});
await appState.setVersion(newVersion);
expect(replaceSpy).toHaveBeenCalledTimes(1);
@@ -469,8 +469,7 @@ describe('AppState', () => {
it('if the current fiddle is an unedited template', async () => {
appState.templateName = oldVersion;
- appState.editorMosaic.set({ [MAIN_JS]: '// content' });
- appState.editorMosaic.isEdited = false;
+ await appState.editorMosaic.set({ [MAIN_JS]: '// content' });
await appState.setVersion(newVersion);
const templateName = newVersion;
@@ -478,8 +477,13 @@ describe('AppState', () => {
});
it('but not if the current fiddle is edited', async () => {
- appState.editorMosaic.set({ [MAIN_JS]: '// content' });
- appState.editorMosaic.isEdited = true;
+ await appState.editorMosaic.set({ [MAIN_JS]: '// content' });
+ (appState.editorMosaic as any).savedHashes = new Map([
+ [MAIN_JS, 'saved'],
+ ]);
+ (appState.editorMosaic as any).currentHashes = new Map([
+ [MAIN_JS, 'different'],
+ ]);
appState.templateName = oldVersion;
await appState.setVersion(newVersion);
@@ -487,7 +491,7 @@ describe('AppState', () => {
});
it('but not if the current fiddle is not a template', async () => {
- appState.editorMosaic.set({ [MAIN_JS]: '// content' });
+ await appState.editorMosaic.set({ [MAIN_JS]: '// content' });
appState.localPath = '/some/path/to/a/fiddle';
await appState.setVersion(newVersion);
@@ -789,7 +793,12 @@ describe('AppState', () => {
it('flags unsaved fiddles', () => {
const expected = `${APPNAME} - Unsaved`;
- appState.editorMosaic.isEdited = true;
+ (appState.editorMosaic as any).savedHashes = new Map([
+ ['main.js', 'saved'],
+ ]);
+ (appState.editorMosaic as any).currentHashes = new Map([
+ ['main.js', 'current'],
+ ]);
const actual = appState.title;
expect(actual).toBe(expected);
});
diff --git a/vitest.config.ts b/vitest.config.ts
index a4c81f75bf..62886d086a 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -4,7 +4,7 @@ export default defineConfig({
test: {
environment: 'jsdom',
globalSetup: 'tests/globalSetup.ts',
- include: ['**/rtl-spec/**/*.spec.*', '**/tests/**/*-spec.{ts,tsx}'],
+ include: ['**/rtl-spec/**/*.spec.{ts,tsx}', '**/tests/**/*-spec.{ts,tsx}'],
setupFiles: ['tests/setup.ts'],
snapshotSerializers: ['enzyme-to-json/serializer'],
},