Files
pybricks-code/src/fileStorage/sagas.test.ts
T
David Lechner a87e2c664c fileStorage: add export actions
These are largely copied from the editor saveAs actions and allow
exporting ("downloading") a file from the browser local storage to
the file system.
2022-03-01 13:44:08 -06:00

234 lines
7.4 KiB
TypeScript

// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import FileSaver from 'file-saver';
import { mock } from 'jest-mock-extended';
import { AsyncSaga } from '../../test';
import {
fileStorageDidChangeItem,
fileStorageDidExportFile,
fileStorageDidFailToExportFile,
fileStorageDidFailToReadFile,
fileStorageDidInitialize,
fileStorageDidReadFile,
fileStorageDidWriteFile,
fileStorageExportFile,
fileStorageReadFile,
fileStorageWriteFile,
} from './actions';
import fileStorage from './sagas';
jest.mock('file-saver');
beforeEach(() => {
// localForge uses localStorage as backend in test environment, so we need
// to start with a clean slate in each test
localStorage.clear();
});
it('should migrate old program from local storage during initialization', async () => {
const oldProgramKey = 'program';
const oldProgramContents = '# test program';
// add item to localStorage to simulate an existing program
localStorage.setItem(oldProgramKey, oldProgramContents);
expect(localStorage.getItem(oldProgramKey)).toBe(oldProgramContents);
const saga = new AsyncSaga(fileStorage);
// initialization should remove the localStorage entry and add add it to
// new storage backend
const action = await saga.take();
expect(action).toEqual(fileStorageDidInitialize(['main.py']));
expect(localStorage.getItem(oldProgramKey)).toBeNull();
await saga.end();
});
it('should read and write files', async () => {
const saga = new AsyncSaga(fileStorage);
let action = await saga.take();
expect(action).toEqual(fileStorageDidInitialize([]));
const testFileName = 'test.file';
const testFileContents = 'test file contents';
// test writing a file
saga.put(fileStorageWriteFile(testFileName, testFileContents));
// writing file triggers response
action = await saga.take();
expect(action).toEqual(fileStorageDidWriteFile(testFileName));
// and as a side-effect, triggers item change as well
action = await saga.take();
expect(action).toEqual(fileStorageDidChangeItem(testFileName));
// test reading the same file back
saga.put(fileStorageReadFile(testFileName));
action = await saga.take();
expect(action).toEqual(fileStorageDidReadFile(testFileName, testFileContents));
await saga.end();
});
it('should dispatch fail action if file does not exist', async () => {
const saga = new AsyncSaga(fileStorage);
let action = await saga.take();
expect(action).toEqual(fileStorageDidInitialize([]));
const testFileName = 'test.file';
saga.put(fileStorageReadFile(testFileName));
action = await saga.take();
expect(fileStorageDidFailToReadFile.matches(action)).toBeTruthy();
await saga.end();
});
describe('export', () => {
/**
* helper function that writes test file to storage for later use in a test
* @param saga The saga.
* @returns The test file name and test file contents.
*/
async function setUpTestFile(saga: AsyncSaga): Promise<[string, string]> {
const testFileName = 'test.file';
const testFileContents = 'test file contents';
const action0 = await saga.take();
expect(action0).toEqual(fileStorageDidInitialize([]));
saga.put(fileStorageWriteFile(testFileName, testFileContents));
const action1 = await saga.take();
expect(action1).toEqual(fileStorageDidWriteFile(testFileName));
const action2 = await saga.take();
expect(action2).toEqual(fileStorageDidChangeItem(testFileName));
return [testFileName, testFileContents];
}
it('should fail if file does not exist', async () => {
const testFileName = 'test.file';
const saga = new AsyncSaga(fileStorage);
const action0 = await saga.take();
expect(action0).toEqual(fileStorageDidInitialize([]));
saga.put(fileStorageExportFile(testFileName));
const action = await saga.take();
expect(action).toEqual(
fileStorageDidFailToExportFile(
testFileName,
new Error('file does not exist'),
),
);
await saga.end();
});
it('should export file with web file system api', async () => {
const saga = new AsyncSaga(fileStorage);
// window.showSaveFilePicker is not defined in the test environment
// so we can't use spyOn().
const mockWriteable = mock<FileSystemWritableFileStream>();
const originalShowSaveFilePicker = window.showSaveFilePicker;
window.showSaveFilePicker = jest.fn().mockResolvedValue(
mock<FileSystemFileHandle>({
createWritable: jest.fn().mockResolvedValue(mockWriteable),
}),
);
const [testFileName] = await setUpTestFile(saga);
saga.put(fileStorageExportFile(testFileName));
const action = await saga.take();
expect(action).toEqual(fileStorageDidExportFile(testFileName));
expect(window.showSaveFilePicker).toHaveBeenCalled();
expect(mockWriteable.write).toHaveBeenCalled();
expect(mockWriteable.close).toHaveBeenCalled();
await saga.end();
window.showSaveFilePicker = originalShowSaveFilePicker;
});
it('should get error from web file system api', async () => {
const saga = new AsyncSaga(fileStorage);
// window.showSaveFilePicker is not defined in the test environment
// so we can't use spyOn().
const testError = new Error('test error');
const originalShowSaveFilePicker = window.showSaveFilePicker;
window.showSaveFilePicker = jest.fn().mockResolvedValue(
mock<FileSystemFileHandle>({
createWritable: jest.fn().mockRejectedValue(testError),
}),
);
const [testFileName] = await setUpTestFile(saga);
saga.put(fileStorageExportFile(testFileName));
const action = await saga.take();
expect(action).toEqual(fileStorageDidFailToExportFile(testFileName, testError));
expect(window.showSaveFilePicker).toHaveBeenCalled();
await saga.end();
window.showSaveFilePicker = originalShowSaveFilePicker;
});
it('should export file using fallback', async () => {
const saga = new AsyncSaga(fileStorage);
const mockFileSaverSaveAs = jest.spyOn(FileSaver, 'saveAs');
const [testFileName] = await setUpTestFile(saga);
saga.put(fileStorageExportFile(testFileName));
const action = await saga.take();
expect(action).toEqual(fileStorageDidExportFile(testFileName));
expect(mockFileSaverSaveAs).toHaveBeenCalled();
await saga.end();
mockFileSaverSaveAs.mockRestore();
});
it('should get error from fallback', async () => {
const saga = new AsyncSaga(fileStorage);
const testError = new Error('test error');
const mockFileSaverSaveAs = jest
.spyOn(FileSaver, 'saveAs')
.mockImplementation(() => {
throw testError;
});
const [testFileName] = await setUpTestFile(saga);
saga.put(fileStorageExportFile(testFileName));
const action = await saga.take();
expect(action).toEqual(fileStorageDidFailToExportFile(testFileName, testError));
expect(mockFileSaverSaveAs).toHaveBeenCalled();
await saga.end();
mockFileSaverSaveAs.mockRestore();
});
});