mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +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
5db22ceb84
commit
51cad751de
@ -56,6 +56,10 @@ describe('Backup and Restore', function () {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
it('should backup the account settings', async 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(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
fixtures: new FixtureBuilder().build(),
|
fixtures: new FixtureBuilder().build(),
|
||||||
@ -97,6 +101,10 @@ describe('Backup and Restore', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should restore the account settings', async 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(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
fixtures: new FixtureBuilder().build(),
|
fixtures: new FixtureBuilder().build(),
|
||||||
|
@ -30,7 +30,12 @@ describe('State logs', function () {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should download state logs for the account', async 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(
|
await withFixtures(
|
||||||
{
|
{
|
||||||
fixtures: new FixtureBuilder().build(),
|
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');
|
const b64 = Buffer.from(data, 'utf8').toString('base64');
|
||||||
// eslint-disable-next-line no-param-reassign
|
const elem = document.createElement('a');
|
||||||
filename = filename || getRandomFileName();
|
elem.href = `data:${contentType};Base64,${b64}`;
|
||||||
const elem = window.document.createElement('a');
|
|
||||||
elem.href = `data:${type};Base64,${b64}`;
|
|
||||||
elem.download = filename;
|
elem.download = filename;
|
||||||
document.body.appendChild(elem);
|
document.body.appendChild(elem);
|
||||||
elem.click();
|
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,
|
MetaMetricsEventName,
|
||||||
} from '../../../../shared/constants/metametrics';
|
} from '../../../../shared/constants/metametrics';
|
||||||
import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../../shared/constants/preferences';
|
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 ActionableMessage from '../../../components/ui/actionable-message';
|
||||||
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
|
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
|
||||||
import { BannerAlert } from '../../../components/component-library';
|
import { BannerAlert } from '../../../components/component-library';
|
||||||
@ -150,7 +153,7 @@ export default class AdvancedTab extends PureComponent {
|
|||||||
|
|
||||||
backupUserData = async () => {
|
backupUserData = async () => {
|
||||||
const { fileName, data } = await this.props.backupUserData();
|
const { fileName, data } = await this.props.backupUserData();
|
||||||
exportAsFile(fileName, data);
|
exportAsFile(fileName, data, ExportableContentType.JSON);
|
||||||
|
|
||||||
this.context.trackEvent({
|
this.context.trackEvent({
|
||||||
event: 'User Data Exported',
|
event: 'User Data Exported',
|
||||||
@ -185,7 +188,11 @@ export default class AdvancedTab extends PureComponent {
|
|||||||
if (err) {
|
if (err) {
|
||||||
displayWarning(t('stateLogError'));
|
displayWarning(t('stateLogError'));
|
||||||
} else {
|
} else {
|
||||||
exportAsFile(`${t('stateLogFileName')}.json`, result);
|
exportAsFile(
|
||||||
|
`${t('stateLogFileName')}.json`,
|
||||||
|
result,
|
||||||
|
ExportableContentType.JSON,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user