1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Add improved downloading logic when exporting state logs (#19872)

* Add improved downloading logic when exporting state logs

* Make test for state logs download only apply to firefox

* Remove eslint override

* Add file extension to test

* Move make jest global.Blob accessible to window
This commit is contained in:
Nicholas Ellul 2023-07-27 13:33:36 -04:00 committed by GitHub
parent cd68bf9d09
commit 90827c54a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 9 deletions

View File

@ -56,6 +56,10 @@ describe('Backup and Restore', function () {
],
};
it('should backup the account settings', async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
// Chrome shows OS level download prompt which can't be dismissed by Selenium
this.skip();
}
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
@ -97,6 +101,10 @@ describe('Backup and Restore', function () {
});
it('should restore the account settings', async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
// Chrome shows OS level download prompt which can't be dismissed by Selenium
this.skip();
}
await withFixtures(
{
fixtures: new FixtureBuilder().build(),

View File

@ -30,7 +30,12 @@ describe('State logs', function () {
},
],
};
it('should download state logs for the account', async function () {
if (process.env.SELENIUM_BROWSER === 'chrome') {
// Chrome shows OS level download prompt which can't be dismissed by Selenium
this.skip();
}
await withFixtures(
{
fixtures: new FixtureBuilder().build(),

View File

@ -1,11 +1,93 @@
import { getRandomFileName } from './util';
/**
* @enum { string }
*/
export const ExportableContentType = {
JSON: 'application/json',
TXT: 'text/plain',
};
export function exportAsFile(filename, data, type = 'text/csv') {
/**
* @enum { string }
*/
const ExtensionForContentType = {
[ExportableContentType.JSON]: '.json',
[ExportableContentType.TXT]: '.txt',
};
/**
* Export data as a file.
*
* @param {string} filename - The name of the file to export.
* @param {string} data - The data to export.
* @param {ExportableContentType} contentType - The content type of the file to export.
*/
export async function exportAsFile(filename, data, contentType) {
if (!ExtensionForContentType[contentType]) {
throw new Error(`Unsupported file type: ${contentType}`);
}
if (supportsShowSaveFilePicker()) {
// Preferred method for downloads
await saveFileUsingFilePicker(filename, data, contentType);
} else {
saveFileUsingDataUri(filename, data, contentType);
}
}
/**
* Notes if the browser supports the File System Access API.
*
* @returns {boolean}
*/
function supportsShowSaveFilePicker() {
return (
typeof window !== 'undefined' &&
typeof window.showSaveFilePicker !== 'undefined' &&
typeof window.Blob !== 'undefined'
);
}
/**
* Saves a file using the File System Access API.
*
* @param {string} filename - The name of the file to export.
* @param {string} data - The data to export.
* @param {ExportableContentType} contentType - The content type of the file to export.
* @returns {Promise<void>}
*/
async function saveFileUsingFilePicker(filename, data, contentType) {
const blob = new window.Blob([data], { contentType });
const fileExtension = ExtensionForContentType[contentType];
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: [
{
description: filename,
accept: {
[contentType]: [fileExtension],
},
},
],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
}
/**
* Saves a file using a data URI.
* This is a fallback for browsers that do not support the File System Access API.
* This method is less preferred because it requires the entire file to be encoded in a data URI.
*
* @param {string} filename - The name of the file to export.
* @param {string} data - The data to export.
* @param {ExportableContentType} contentType - The content type of the file to export.
*/
function saveFileUsingDataUri(filename, data, contentType) {
const b64 = Buffer.from(data, 'utf8').toString('base64');
// eslint-disable-next-line no-param-reassign
filename = filename || getRandomFileName();
const elem = window.document.createElement('a');
elem.href = `data:${type};Base64,${b64}`;
const elem = document.createElement('a');
elem.href = `data:${contentType};Base64,${b64}`;
elem.download = filename;
document.body.appendChild(elem);
elem.click();

View File

@ -0,0 +1,68 @@
import { exportAsFile, ExportableContentType } from './export-utils';
describe('exportAsFile', () => {
let windowSpy;
beforeEach(() => {
windowSpy = jest.spyOn(window, 'window', 'get');
});
afterEach(() => {
windowSpy.mockRestore();
});
describe('when showSaveFilePicker is supported', () => {
it('uses .json file extension when content type is JSON', async () => {
const showSaveFilePicker = mockShowSaveFilePicker();
const filename = 'test.json';
const data = '{file: "content"}';
windowSpy.mockImplementation(() => ({
showSaveFilePicker,
Blob: global.Blob,
}));
await exportAsFile(filename, data, ExportableContentType.JSON);
expect(showSaveFilePicker).toHaveBeenCalledWith({
suggestedName: filename,
types: [
{
description: filename,
accept: { 'application/json': ['.json'] },
},
],
});
});
it('uses .txt file extension when content type is TXT', async () => {
const showSaveFilePicker = mockShowSaveFilePicker();
const filename = 'test.txt';
const data = 'file content';
windowSpy.mockImplementation(() => ({
showSaveFilePicker,
Blob: global.Blob,
}));
await exportAsFile(filename, data, ExportableContentType.TXT);
expect(showSaveFilePicker).toHaveBeenCalledWith({
suggestedName: filename,
types: [
{
description: filename,
accept: { 'text/plain': ['.txt'] },
},
],
});
});
});
});
function mockShowSaveFilePicker() {
return jest.fn().mockResolvedValueOnce({
createWritable: jest
.fn()
.mockResolvedValueOnce({ write: jest.fn(), close: jest.fn() }),
});
}

View File

@ -23,7 +23,10 @@ import {
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../../shared/constants/preferences';
import { exportAsFile } from '../../../helpers/utils/export-utils';
import {
exportAsFile,
ExportableContentType,
} from '../../../helpers/utils/export-utils';
import ActionableMessage from '../../../components/ui/actionable-message';
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
import { BannerAlert } from '../../../components/component-library';
@ -150,7 +153,7 @@ export default class AdvancedTab extends PureComponent {
backupUserData = async () => {
const { fileName, data } = await this.props.backupUserData();
exportAsFile(fileName, data);
exportAsFile(fileName, data, ExportableContentType.JSON);
this.context.trackEvent({
event: 'User Data Exported',
@ -185,7 +188,11 @@ export default class AdvancedTab extends PureComponent {
if (err) {
displayWarning(t('stateLogError'));
} else {
exportAsFile(`${t('stateLogFileName')}.json`, result);
exportAsFile(
`${t('stateLogFileName')}.json`,
result,
ExportableContentType.JSON,
);
}
});
}}