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:
parent
cd68bf9d09
commit
90827c54a1
@ -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(),
|
||||
|
@ -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(),
|
||||
|
@ -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();
|
||||
|
68
ui/helpers/utils/export-utils.test.js
Normal file
68
ui/helpers/utils/export-utils.test.js
Normal 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() }),
|
||||
});
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
Loading…
Reference in New Issue
Block a user