Skip to content

Commit

Permalink
- ignore coverage directory
Browse files Browse the repository at this point in the history
- add coverage script in package.json
- add more tests
- clean up
  • Loading branch information
ingorichter committed Jun 5, 2017
1 parent 3d094c0 commit 3d55d63
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 70 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist/
node_modules/
coverage/
tabwrangler*.zip
176 changes: 141 additions & 35 deletions __tests__/importExport.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import {importData, exportData, exportFileName} from '../app/js/importExport';
import storageLocal from '../app/js/storageLocal';
import { importData, exportData, exportFileName } from '../app/js/importExport';
import FileSaver from 'file-saver';

beforeEach(() => {
window.chrome = {
'storage': {
'local': {
},
},
'extension': {
getBackgroundPage: () => {
return {
'TW': storageLocal,
};
},
},
};
});

Expand All @@ -14,56 +23,153 @@ afterEach(() => {
})

test('should export the bookmark data', () => {
// provide a mock function
const mockFunction = jest.fn();
window.chrome.storage.local.get = mockFunction;
const mockValues = {
'totalTabsRemoved': 256,
'totalTabsUnwrangled': 120,
'totalTabsWrangled': 100,
};

// provide some mock functions
const localStorageGet = jest.fn((key) => mockValues[key]);
const fileSaveMock = jest.fn();

exportData();
expect(mockFunction.mock.calls.length).toBe(4);
window.chrome.storage.local.get = (t, func) => {
func(null, {test: 2});
};

const storageLocal = {
get: localStorageGet,
}

FileSaver.saveAs = fileSaveMock;

exportData(storageLocal);
expect(localStorageGet.mock.calls.length).toBe(3);
expect(fileSaveMock.mock.calls.length).toBe(1);
const result = fileSaveMock.mock.calls[0][0];
console.log(result.type);
expect(result.type).toBe('application/json;charset=utf-8');
});

test('should import the bookmark data', (done) => {
// provide a mock function
const mockFunction = jest.fn();
window.chrome.storage.local.set = mockFunction;
const localStorageSetMock = jest.fn();
window.chrome.storage.local.set = localStorageSetMock;
const tabManagerInit = jest.fn();

const expectedImportData = {
'savedTabs': [{
'active': false,
'audible': false,
'autoDiscardable': true,
'closedAt': 1493418190099,
'discarded': false,
'height': 175,
'highlighted': false,
'id': 36,
'incognito': false,
'index': 1,
'mutedInfo': {
'muted': false,
savedTabs: [
{
active: false,
audible: false,
autoDiscardable: true,
closedAt: 1493418190099,
discarded: false,
height: 175,
highlighted: false,
id: 36,
incognito: false,
index: 1,
mutedInfo: {
muted: false,
},
pinned: false,
selected: false,
status: 'complete',
title: 'fish: Tutorial',
url: 'https://fishshell.com/docs/current/tutorial.html',
width: 400,
windowId: 33,
},
'pinned': false,
'selected': false,
'status': 'complete',
'title': 'fish: Tutorial',
'url': 'https://fishshell.com/docs/current/tutorial.html',
'width': 400,
'windowId': 33,
}],
'totalTabsRemoved': 256,
'totalTabsUnwrangled': 16,
'totalTabsWrangled': 32,
],
totalTabsRemoved: 256,
totalTabsUnwrangled: 16,
totalTabsWrangled: 32,
};

const blob = new Blob([JSON.stringify(expectedImportData)], {
type: 'text/plain;charset=utf-8',
});

importData({target: {
importData(storageLocal, {closedTabs: {init: tabManagerInit}}, {target: {
files: [blob],
}}).then(() => {
expect(mockFunction).toBeCalled();
expect(mockFunction).toBeCalledWith(expectedImportData);
expect(localStorageSetMock.mock.calls.length).toBe(4);
expect(localStorageSetMock.mock.calls[3][0]).toEqual({savedTabs: expectedImportData.savedTabs});
expect(localStorageSetMock.mock.calls[0][0]).toEqual({totalTabsRemoved: expectedImportData.totalTabsRemoved});
expect(localStorageSetMock.mock.calls[1][0]).toEqual({totalTabsUnwrangled: expectedImportData.totalTabsUnwrangled});
expect(localStorageSetMock.mock.calls[2][0]).toEqual({totalTabsWrangled: expectedImportData.totalTabsWrangled});

done();
}).catch((e) => console.error(e));
});

test('should fail to import non existent backup', done => {
// provide a mock function
const mockFunction = jest.fn();
window.chrome.storage.local.set = mockFunction;

importData(
storageLocal,
{},
{
target: {
files: [],
},
}
).catch(() => {
expect(mockFunction.mock.calls.length).toBe(0);

done();
});
});

test('should fail import of incomplete backup data', done => {
// provide a mock function
const mockFunction = jest.fn();
window.chrome.storage.local.set = mockFunction;

// this is missing the savedTabs object
const expectedImportData = [
{ totalTabsRemoved: 256 },
{ totalTabsUnwrangled: 16 },
{ totalTabsWrangled: 32 },
];

const blob = new Blob([JSON.stringify(expectedImportData)], {
type: 'text/plain;charset=utf-8',
});

importData(storageLocal, {}, {
target: {
files: [blob],
},
}).catch(() => {
expect(mockFunction.mock.calls.length).toBe(0);

done();
});
});

test('should fail import of corrupt backup data', done => {
// provide a mock function
const mockFunction = jest.fn();
window.chrome.storage.local.set = mockFunction;

const blob = new Blob(['{345:}'], {
type: 'text/plain;charset=utf-8',
});

importData(
storageLocal,
{},
{
target: {
files: [blob],
},
}
).catch(() => {
expect(mockFunction.mock.calls.length).toBe(0);

done();
});
Expand Down
37 changes: 37 additions & 0 deletions __tests__/storageLocal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import storageLocal from '../app/js/storageLocal';

beforeEach(() => {
window.chrome = {
storage: {
local: {},
onChanged: {
addListener() {
},
},
},
};
});

afterEach(() => {
window.chrome = {};
});

test('should refresh cache after localstorage changed', () => {
const mockFunctionGet = jest.fn();
const mockFunctionSet = jest.fn();
window.chrome.storage.local.set = mockFunctionSet;
window.chrome.storage.local.get = mockFunctionGet;

// initialize cache and localStarage change listener
storageLocal.init();

// update local storage with default entries
expect(Object.keys(storageLocal.cache).length).toBe(4);

// replace the savedTabs
storageLocal.setValue('savedTabs', [{'test': 'new Value'}]);

// cache must be updated
expect(Object.keys(storageLocal.cache).length).toBe(5);
expect(storageLocal.cache['savedTabs']).toEqual([{ test: 'new Value' }]);
});
76 changes: 49 additions & 27 deletions app/js/importExport.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import FileSaver from 'file-saver'

function readLocalStorage(key) {
return new Promise((resolve) => {
chrome.storage.local.get(key, (value) => {
resolve(value);
});
});
}

const importData = (event) => {
/**
* Import the backup of saved tabs and the accounting information.
* If any of the required keys in the backup object is missing, the backup will abort without importing the data.
* @param {storageLocal} storageLocal is needed to restore the accounting information
* @param {tabManager} tabManager is required to initialize it with the imported saved tabs
* @param {Event} event contains the path of the backup file
*/
const importData = (storageLocal, tabManager, event) => {
const files = event.target.files;

if (files[0]) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = () => {
try {
chrome.storage.local.set(JSON.parse(fileReader.result));
resolve();
const json = JSON.parse(fileReader.result);
if(Object.keys(json).length < 4) {
reject('Invalid backup');
} else {
const savedTabs = json.savedTabs;
const totalTabsRemoved = json.totalTabsRemoved;
const totalTabsUnwrangled = json.totalTabsUnwrangled;
const totalTabsWrangled = json.totalTabsWrangled;

storageLocal.setValue('totalTabsRemoved', totalTabsRemoved);
storageLocal.setValue('totalTabsUnwrangled', totalTabsUnwrangled);
storageLocal.setValue('totalTabsWrangled', totalTabsWrangled);

chrome.storage.local.set({savedTabs});
// re-read the wrangled tabs
tabManager.closedTabs.init();
resolve();
}
} catch (e) {
reject(e);
}
Expand All @@ -30,28 +45,35 @@ const importData = (event) => {
fileReader.readAsText(files[0], 'utf-8');
});
} else {
console.log('Nothing to import');

return Promise.resolve();
return Promise.reject('Nothing to import');
}
}
/**
* Export all saved tabs and some accounting information in one object. The object has 4 keys
* - savedTabs
* - totalTabsRemoved
* - totalTabsUnwrangled
* - totalTabsWrangled
*
* savedTabs is acquired by reading it directly from localstorage.
*
* @param {storageLocal} storageLocal to retrieve all the accounting information
*/
const exportData = (storageLocal) => {
chrome.storage.local.get('savedTabs', (err, savedTabs) => {
if (!err) {
savedTabs['totalTabsRemoved'] = storageLocal.get('totalTabsRemoved');
savedTabs['totalTabsUnwrangled'] = storageLocal.get('totalTabsUnwrangled');
savedTabs['totalTabsWrangled'] = storageLocal.get('totalTabsWrangled');

const exportData = JSON.stringify(savedTabs);

const exportData = () => {
// since there is storageLocal, I don't know if it would be better to put
// that function call there
Promise.all([
readLocalStorage('savedTabs'),
readLocalStorage('totalTabsRemoved'),
readLocalStorage('totalTabsUnwrangled'),
readLocalStorage('totalTabsWrangled')]).then((allValues) => {
// allValues is an array containing all the values stored in local storage
const _result = JSON.stringify(allValues);

const blob = new Blob([_result], {
const blob = new Blob([exportData], {
type: 'application/json;charset=utf-8',
});
FileSaver.saveAs(blob, exportFileName(new Date(Date.now())));
});
}
});
}

const exportFileName = (date) => {
Expand Down
10 changes: 7 additions & 3 deletions app/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const {
tabmanager,
} = TW;

// curry import/export function with storageLocal
const _importData = _.partial(importData, storageLocal, tabmanager);
const _exportData = _.partial(exportData, storageLocal);

function secondsToMinutes(seconds) {
let s = seconds % 60;
s = s >= 10 ? String(s) : `0${String(s)}`;
Expand Down Expand Up @@ -486,16 +490,16 @@ class OptionsTab extends React.Component {
<h4 className="page-header">Import / Export</h4>
<div className="row">
<div className="col-xs-8">
<Button label='Export' clickHandler={exportData} className='glyphicon-export'/>
<Button label='Export' clickHandler={_exportData} className='glyphicon-export'/>
<Button label='Import' clickHandler={() => {this.fileselector.click()}} className='glyphicon-import'/>
<input id="fileselector" type="file" onChange={importData} ref={(input) => {this.fileselector = input}}/>
<input id="fileselector" type="file" onChange={_importData} ref={(input) => {this.fileselector = input}}/>
</div>
<div className="col-xs-8">
<p className="help-block">
Export all information about wrangled tabs. This is a convenient way to restore an old state after reinstalling the extension.
</p>
<p className="help-block">
<strong>Warning:</strong> Importing data will overwrite all existing data. There is no way back (unless you have a backup).
<strong>Warning:</strong> Importing data will overwrite all existing data. There is no way back (unless you have a backup).
</p>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ gulp.task('cp-lib', function() {
});

gulp.task('lint', function() {
return gulp.src(['**/*.js', `!${DIST_DIRECTORY}/**`, '!node_modules/**'])
return gulp.src(['**/*.js', `!${DIST_DIRECTORY}/**`, '!node_modules/**', '!coverage/**'])
// eslint() attaches the lint output to the "eslint" property
// of the file object so it can be used by other modules.
.pipe(eslint())
Expand Down
Loading

0 comments on commit 3d55d63

Please sign in to comment.