From f9c1f7084ce1da2d062e99688797106859dde44a Mon Sep 17 00:00:00 2001 From: David Lechner Date: Wed, 16 Mar 2022 14:59:10 -0500 Subject: [PATCH] drop FileSaver browser-fs-access already includes fallbacks to the file storage api so we don't have to provide our own. --- package.json | 1 - src/fileStorage/sagas.test.ts | 164 ++++------------------------------ src/fileStorage/sagas.ts | 90 ++++++------------- yarn.lock | 8 -- 4 files changed, 46 insertions(+), 217 deletions(-) diff --git a/package.json b/package.json index b46dabea..1f60d711 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "browser-fs-access": "^0.25.0", "canvas": "^2.9.0", "copy-webpack-plugin": "^6.4.1", - "file-saver": "^2.0.5", "jszip": "^3.7.1", "license-webpack-plugin": "^3.0.0", "localforage": "^1.10.0", diff --git a/src/fileStorage/sagas.test.ts b/src/fileStorage/sagas.test.ts index 4c8e8d3b..c989cd2a 100644 --- a/src/fileStorage/sagas.test.ts +++ b/src/fileStorage/sagas.test.ts @@ -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(); - const originalShowSaveFilePicker = window.showSaveFilePicker; - window.showSaveFilePicker = jest.fn().mockResolvedValue( - mock({ - 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({ - 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(); - const originalShowSaveFilePicker = window.showSaveFilePicker; - window.showSaveFilePicker = jest.fn().mockResolvedValue( - mock({ - 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({ - 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(); }); }); diff --git a/src/fileStorage/sagas.ts b/src/fileStorage/sagas.ts index b44e174b..b042db2e 100644 --- a/src/fileStorage/sagas.ts +++ b/src/fileStorage/sagas.ts @@ -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) { diff --git a/yarn.lock b/yarn.lock index 9c77b9cf..bbff0727 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2139,7 +2139,6 @@ __metadata: eslint-plugin-prettier: ^4.0.0 eslint-plugin-react: ^7.29.4 eslint-plugin-total-functions: ^4.10.1 - file-saver: ^2.0.5 jest-mock-extended: ^2.0.4 jszip: ^3.7.1 license-webpack-plugin: ^3.0.0 @@ -7599,13 +7598,6 @@ __metadata: languageName: node linkType: hard -"file-saver@npm:^2.0.5": - version: 2.0.5 - resolution: "file-saver@npm:2.0.5" - checksum: c62d96e5cebc58b4bdf3ae8a60d5cf9607ad82f75f798c33a4ee63435ac2203002584d5256a2a780eda7feb5e19dc3b6351c2212e58b3f529e63d265a7cc79f7 - languageName: node - linkType: hard - "file-selector@npm:^0.4.0": version: 0.4.0 resolution: "file-selector@npm:0.4.0"