Files
pybricks-code/src/editor/sagas.test.ts
T
David Lechner 41e826d73b editor: save view state on change
Instead of trying to save the view state when a window "closes", which
is unreliable, we just save the state on every change.
2022-05-31 17:43:54 -05:00

292 lines
10 KiB
TypeScript

// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { mock } from 'jest-mock-extended';
import { monaco } from 'react-monaco-editor';
import { AsyncSaga, uuid } from '../../test';
import {
fileStorageDidFailToLoadTextFile,
fileStorageDidInitialize,
fileStorageDidLoadTextFile,
fileStorageLoadTextFile,
fileStorageStoreTextFileValue,
} from '../fileStorage/actions';
import { acquireLock } from '../utils';
import {
editorActivateFile,
editorCloseFile,
editorDidActivateFile,
editorDidCloseFile,
editorDidCreate,
editorDidFailToActivateFile,
editorDidFailToOpenFile,
editorDidOpenFile,
editorOpenFile,
} from './actions';
import { EditorError, EditorErrorName } from './error';
import { ActiveFileHistoryManager, OpenFileInfo, OpenFileManager } from './lib';
import editor from './sagas';
jest.mock('./lib');
const testFileUuid = uuid(0);
const newTestFileUuid = uuid(1);
afterEach(() => {
jest.resetAllMocks();
sessionStorage.clear();
});
it('should activate files from storage', async () => {
jest.spyOn(
ActiveFileHistoryManager.prototype,
'getFromStorage',
).mockReturnValueOnce([testFileUuid].values());
const saga = new AsyncSaga(editor);
monaco.editor.create(document.createElement('div'));
saga.put(fileStorageDidInitialize([]));
await expect(saga.take()).resolves.toEqual(editorDidCreate());
// other editor actions on create should take place after editorDidCreate()
await expect(saga.take()).resolves.toEqual(editorActivateFile(testFileUuid));
await saga.end();
});
/**
* Asymmetric matcher for matching errors by name while ignoring the message.
* @param name The name to match.
* @returns An asymmetric matcher cast to an EditorError so that it can be
* passed to action functions.
*/
function expectEditorError(name: EditorErrorName): EditorError {
const matcher: jest.AsymmetricMatcher & Record<string, unknown> = {
$$typeof: Symbol.for('jest.asymmetricMatcher'),
asymmetricMatch: (other) => other instanceof EditorError && other.name === name,
toAsymmetricMatcher: () => `[EditorError: ${name}]`,
};
return matcher as unknown as EditorError;
}
describe('per-editor sagas', () => {
let saga: AsyncSaga;
let monacoEditor: monaco.editor.IStandaloneCodeEditor;
beforeEach(async () => {
jest.spyOn(
ActiveFileHistoryManager.prototype,
'getFromStorage',
).mockReturnValueOnce([].values());
saga = new AsyncSaga(editor);
saga.updateState({ fileStorage: { isInitialized: true } });
monacoEditor = monaco.editor.create(document.createElement('div'));
await expect(saga.take()).resolves.toEqual(editorDidCreate());
});
describe('handleEditorOpenFile', () => {
it('should fail if file is already in use', async () => {
const releaseLock = await acquireLock(
`pybricks.editor+pybricksCode:${testFileUuid}`,
);
expect(releaseLock).toBeDefined();
try {
saga.put(editorOpenFile(testFileUuid));
await expect(saga.take()).resolves.toEqual(
editorDidFailToOpenFile(
testFileUuid,
expectEditorError('FileInUse'),
),
);
} finally {
await releaseLock?.();
}
});
describe('not already in use', () => {
beforeEach(async () => {
jest.spyOn(OpenFileManager.prototype, 'add');
jest.spyOn(OpenFileManager.prototype, 'remove');
saga.put(editorOpenFile(testFileUuid));
await expect(saga.take()).resolves.toEqual(
fileStorageLoadTextFile(testFileUuid),
);
});
it('should propagate error from fileStorageLoadTextFile', async () => {
const testError = new Error('test error');
saga.put(fileStorageDidFailToLoadTextFile(testFileUuid, testError));
await expect(saga.take()).resolves.toEqual(
editorDidFailToOpenFile(testFileUuid, testError),
);
expect(OpenFileManager.prototype.add).not.toHaveBeenCalled();
});
describe('read succeeded', () => {
let model: monaco.editor.ITextModel;
beforeEach(async () => {
monaco.editor.onDidCreateModel((m) => (model = m));
saga.put(fileStorageDidLoadTextFile(testFileUuid, '', null));
expect(model).toBeDefined();
await expect(saga.take()).resolves.toEqual(
editorDidOpenFile(testFileUuid),
);
expect(OpenFileManager.prototype.add).toHaveBeenCalled();
expect(OpenFileManager.prototype.remove).not.toHaveBeenCalled();
});
it('should close file if task is canceled', async () => {
jest.spyOn(model, 'dispose');
saga.cancel();
// model should be disposed before fileStorageClose
expect(model.dispose).toHaveBeenCalled();
expect(OpenFileManager.prototype.remove).toHaveBeenCalled();
// editorDidCloseFile is not called since we did not put editorCloseFile
});
it('should close when requested', async () => {
jest.spyOn(model, 'dispose');
saga.put(editorCloseFile(testFileUuid));
// file is saved on close
await expect(saga.take()).resolves.toEqual(
fileStorageStoreTextFileValue(testFileUuid, ''),
);
// model should be disposed before fileStorageClose
expect(model.dispose).toHaveBeenCalled();
expect(OpenFileManager.prototype.remove).toHaveBeenCalled();
await expect(saga.take()).resolves.toEqual(
editorDidCloseFile(testFileUuid),
);
});
});
});
});
describe('handleEditorActivateFile', () => {
describe('file is not already open', () => {
beforeEach(async () => {
jest.spyOn(OpenFileManager.prototype, 'has').mockReturnValueOnce(false);
saga.put(editorActivateFile(testFileUuid));
await expect(saga.take()).resolves.toEqual(
editorOpenFile(testFileUuid),
);
});
it('should propagate error if open fails', async () => {
const testError = new Error('test error');
saga.put(editorDidFailToOpenFile(testFileUuid, testError));
await expect(saga.take()).resolves.toEqual(
editorDidFailToActivateFile(testFileUuid, testError),
);
});
it('should complete if open succeeds', async () => {
jest.spyOn(monacoEditor, 'getModel').mockReturnValueOnce(null);
jest.spyOn(monacoEditor, 'setModel').mockReturnValueOnce();
jest.spyOn(monacoEditor, 'restoreViewState').mockReturnValueOnce();
jest.spyOn(ActiveFileHistoryManager.prototype, 'push');
jest.spyOn(OpenFileManager.prototype, 'get').mockReturnValueOnce(
mock<OpenFileInfo>(),
);
jest.spyOn(OpenFileManager.prototype, 'updateViewState');
saga.put(editorDidOpenFile(testFileUuid));
// changes should be made before editorDidActivateFile
expect(
OpenFileManager.prototype.updateViewState,
).not.toHaveBeenCalled();
expect(monacoEditor.setModel).toHaveBeenCalled();
expect(monacoEditor.restoreViewState).toHaveBeenCalled();
expect(ActiveFileHistoryManager.prototype.push).toHaveBeenCalled();
await expect(saga.take()).resolves.toEqual(
editorDidActivateFile(testFileUuid),
);
});
});
describe('file is already open', () => {
beforeEach(async () => {
jest.spyOn(OpenFileManager.prototype, 'has').mockReturnValueOnce(true);
jest.spyOn(OpenFileManager.prototype, 'get').mockReturnValueOnce(
mock<OpenFileInfo>(),
);
jest.spyOn(OpenFileManager.prototype, 'updateViewState');
jest.spyOn(monacoEditor, 'getModel').mockReturnValueOnce(null);
jest.spyOn(monacoEditor, 'setModel').mockReturnValueOnce();
jest.spyOn(monacoEditor, 'restoreViewState').mockReturnValueOnce();
saga.put(editorActivateFile(testFileUuid));
});
it('should set the model and update sessionStorage', async () => {
// changes should be made before editorDidActivateFile
expect(monacoEditor.setModel).toHaveBeenCalled();
expect(monacoEditor.restoreViewState).toHaveBeenCalled();
expect(
OpenFileManager.prototype.updateViewState,
).not.toHaveBeenCalled();
await expect(saga.take()).resolves.toEqual(
editorDidActivateFile(testFileUuid),
);
});
});
});
describe('handleEditorDidCloseFile', () => {
it('should activate a new file if closed file was currently active', async () => {
jest.spyOn(ActiveFileHistoryManager.prototype, 'pop').mockReturnValueOnce(
newTestFileUuid,
);
saga.put(editorDidCloseFile(testFileUuid));
expect(ActiveFileHistoryManager.prototype.pop).toHaveBeenCalled();
await expect(saga.take()).resolves.toEqual(
editorActivateFile(newTestFileUuid),
);
});
it('should do nothing if closed file was not currently active', async () => {
jest.spyOn(ActiveFileHistoryManager.prototype, 'pop').mockReturnValueOnce(
undefined,
);
saga.put(editorDidCloseFile(testFileUuid));
expect(ActiveFileHistoryManager.prototype.pop).toHaveBeenCalled();
});
});
afterEach(async () => {
await saga.end();
});
});