drop FileSaver

browser-fs-access already includes fallbacks to the file storage api
so we don't have to provide our own.
This commit is contained in:
David Lechner
2022-03-16 14:59:10 -05:00
parent 6062517dec
commit f9c1f7084c
4 changed files with 46 additions and 217 deletions
+19 -145
View File
@@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import FileSaver from 'file-saver';
import { mock } from 'jest-mock-extended';
import * as browserFsAccess from 'browser-fs-access';
import { AsyncSaga } from '../../test';
import {
fileStorageArchiveAllFiles,
@@ -26,9 +25,10 @@ import {
} from './actions';
import fileStorage from './sagas';
jest.mock('file-saver');
jest.mock('browser-fs-access');
beforeEach(() => {
jest.clearAllMocks();
// localForge uses localStorage as backend in test environment, so we need
// to start with a clean slate in each test
localStorage.clear();
@@ -183,195 +183,69 @@ describe('export', () => {
await saga.end();
});
it('should export file with web file system api', async () => {
it('should export file', 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);
jest.spyOn(browserFsAccess, 'fileSave');
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();
expect(browserFsAccess.fileSave).toHaveBeenCalled();
await saga.end();
window.showSaveFilePicker = originalShowSaveFilePicker;
});
it('should get error from web file system api', async () => {
it('should catch error', 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);
const testError = new Error('test error');
jest.spyOn(browserFsAccess, 'fileSave').mockRejectedValue(testError);
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();
});
});
describe('archive', () => {
it('should archive file with web file system api', async () => {
it('should archive file', 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),
}),
);
await setUpTestFile(saga);
jest.spyOn(browserFsAccess, 'fileSave');
saga.put(fileStorageArchiveAllFiles());
const action = await saga.take();
expect(action).toEqual(fileStorageDidArchiveAllFiles());
expect(window.showSaveFilePicker).toHaveBeenCalled();
expect(mockWriteable.write).toHaveBeenCalled();
expect(mockWriteable.close).toHaveBeenCalled();
expect(browserFsAccess.fileSave).toHaveBeenCalled();
await saga.end();
window.showSaveFilePicker = originalShowSaveFilePicker;
});
it('should get error from web file system api', async () => {
it('should catch error', 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),
}),
);
await setUpTestFile(saga);
const testError = new Error('test error');
jest.spyOn(browserFsAccess, 'fileSave').mockRejectedValue(testError);
saga.put(fileStorageArchiveAllFiles());
const action = await saga.take();
expect(action).toEqual(fileStorageDidFailToArchiveAllFiles(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');
await setUpTestFile(saga);
saga.put(fileStorageArchiveAllFiles());
const action = await saga.take();
expect(action).toEqual(fileStorageDidArchiveAllFiles());
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;
});
await setUpTestFile(saga);
saga.put(fileStorageArchiveAllFiles());
const action = await saga.take();
expect(action).toEqual(fileStorageDidFailToArchiveAllFiles(testError));
expect(mockFileSaverSaveAs).toHaveBeenCalled();
await saga.end();
mockFileSaverSaveAs.mockRestore();
});
});
+27 -63
View File
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import FileSaver from 'file-saver';
import { fileSave } from 'browser-fs-access';
import JSZip from 'jszip';
import localForage from 'localforage';
import { extendPrototype } from 'localforage-observable';
@@ -110,47 +110,24 @@ function* handleExportFile(
return;
}
const blob = new Blob([data], { type: `${pythonFileMimeType};charset=utf-8` });
const blob = new Blob([data], { type: `${pythonFileMimeType}` });
if (window.showSaveFilePicker) {
// This uses https://wicg.github.io/file-system-access which is not
// available in all browsers
try {
const handle = yield* call(() =>
window.showSaveFilePicker({
suggestedName: action.fileName,
types: [
{
accept: { [pythonFileMimeType]: pythonFileExtension },
// TODO: translate description
description: 'Python Files',
},
],
}),
);
try {
yield* call(() =>
fileSave(blob, {
id: 'pybricksCodeFileStorageExport',
fileName: action.fileName,
extensions: [pythonFileExtension],
mimeTypes: [pythonFileMimeType],
// TODO: translate description
description: 'Python Files',
}),
);
const writeable = yield* call(() => handle.createWritable());
yield* call(() => writeable.write(blob));
yield* call(() => writeable.close());
} catch (err) {
yield* put(
fileStorageDidFailToExportFile(action.fileName, ensureError(err)),
);
return;
}
} else {
// this is a fallback to use the standard browser download mechanism
try {
FileSaver.saveAs(blob, action.fileName);
} catch (err) {
yield* put(
fileStorageDidFailToExportFile(action.fileName, ensureError(err)),
);
return;
}
yield* put(fileStorageDidExportFile(action.fileName));
} catch (err) {
yield* put(fileStorageDidFailToExportFile(action.fileName, ensureError(err)));
}
yield* put(fileStorageDidExportFile(action.fileName));
}
/**
@@ -214,31 +191,18 @@ function* handleArchiveAllFiles(files: LocalForage): Generator {
const zipData = yield* call(() => zip.generateAsync({ type: 'blob' }));
const suggestedName = `pybricks-backup-${timestamp()}.zip`;
const fileName = `pybricks-backup-${timestamp()}.zip`;
if (window.showSaveFilePicker) {
// This uses https://wicg.github.io/file-system-access which is not
// available in all browsers
const handle = yield* call(() =>
window.showSaveFilePicker({
suggestedName,
types: [
{
accept: { 'application/zip': '.zip' },
// TODO: translate description
description: 'Zip Files',
},
],
}),
);
const writeable = yield* call(() => handle.createWritable());
yield* call(() => writeable.write(zipData));
yield* call(() => writeable.close());
} else {
// this is a fallback to use the standard browser download mechanism
FileSaver.saveAs(zipData, suggestedName);
}
yield* call(() =>
fileSave(zipData, {
id: 'pybricksCodeFileStorageArchive',
fileName,
extensions: ['.zip'],
mimeTypes: ['application/zip'],
// TODO: translate description
description: 'Zip Files',
}),
);
yield* put(fileStorageDidArchiveAllFiles());
} catch (err) {