blob: 6708e9fdb4f2af67584148f374e4c9985c64a960 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
goog.setTestOnly('persistent_data_test');
goog.require('mr.PersistentData');
goog.require('mr.PersistentDataManager');
goog.require('mr.UnitTestUtils');
/**
* @implements {mr.PersistentData}
* @private
*/
DummyData_ = class {
/**
* @param {string} id
*/
constructor(id) {
/** @private {string} */
this.id_ = id;
}
/**
* @override
*/
getData() {
return [];
}
/**
* @override
*/
getStorageKey() {
return 'dummy-data' + this.id_;
}
/**
* @override
*/
loadSavedData() {}
};
describe('Tests PersistentDataManager', () => {
let dataInstance1;
let dataInstance2;
let onSuspendListener;
let version;
let mrInstanceId;
const originalQuota = mr.PersistentDataManager.QUOTA_CHARS;
beforeEach(() => {
window.localStorage.clear();
mr.PersistentDataManager.charsUsed_ = 0;
dataInstance1 = new DummyData_('1');
dataInstance2 = new DummyData_('2');
version = '1.0';
mrInstanceId = '123';
mr.UnitTestUtils.mockChromeApi();
chrome.runtime.onSuspend = {
addListener: l => {
onSuspendListener = l;
}
};
chrome.runtime.getManifest = () => ({'version': version});
mr.PersistentDataManager.initialize(mrInstanceId);
dataInstance1.loadSavedData = jasmine.createSpy('loadSavedData');
dataInstance2.loadSavedData = jasmine.createSpy('loadSavedData');
mr.PersistentDataManager.register(dataInstance1);
mr.PersistentDataManager.register(dataInstance2);
expect(dataInstance1.loadSavedData).toHaveBeenCalled();
expect(dataInstance2.loadSavedData).toHaveBeenCalled();
});
afterEach(() => {
mr.PersistentDataManager.clear();
mr.PersistentDataManager.QUOTA_CHARS = originalQuota;
mr.UnitTestUtils.restoreChromeApi();
});
it('returns null with no saved data', () => {
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toBe(null);
expect(mr.PersistentDataManager.getTemporaryData(dataInstance2)).toBe(null);
});
describe('handles onSuspend', () => {
it('with one instance with data', () => {
dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
dataInstance2.getData = () => null;
onSuspendListener();
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
'd': 1
});
expect(mr.PersistentDataManager.getPersistentData(dataInstance1))
.toEqual({'e': 1});
expect(mr.PersistentDataManager.getTemporaryData(dataInstance2))
.toBe(null);
expect(mr.PersistentDataManager.charsUsed_).toBe(83);
});
it('with two instances with data', () => {
dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
dataInstance2.getData = () => [{'d': 2}, {'e': 2}];
onSuspendListener();
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
'd': 1
});
expect(mr.PersistentDataManager.getPersistentData(dataInstance1))
.toEqual({'e': 1});
expect(mr.PersistentDataManager.getTemporaryData(dataInstance2)).toEqual({
'd': 2
});
expect(mr.PersistentDataManager.getPersistentData(dataInstance2))
.toEqual({'e': 2});
expect(mr.PersistentDataManager.charsUsed_).toBe(141);
});
it('with no instance with data', () => {
dataInstance1.getData = () => null;
dataInstance2.getData = () => null;
onSuspendListener();
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1))
.toBe(null);
expect(mr.PersistentDataManager.getTemporaryData(dataInstance2))
.toBe(null);
expect(mr.PersistentDataManager.charsUsed_).toBe(25);
});
it('with an instance with a circular dependency', () => {
const data1 = {};
data1.data = data1;
dataInstance1.getData = () => [{'d': data1}, {'e': data1}];
dataInstance2.getData = () => [{'d': 2}, {'e': 2}];
onSuspendListener();
// If there is a circular dependency of objects, that data cannot be
// converted into JSON or saved. However other data should still be saved.
expect(mr.PersistentDataManager.getTemporaryData(dataInstance2)).toEqual({
'd': 2
});
expect(mr.PersistentDataManager.getPersistentData(dataInstance2))
.toEqual({'e': 2});
});
});
it('Test saveData', () => {
dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
dataInstance2.getData = () => [{'d': 2}, {'e': 2}];
const dataInstance3 = new DummyData_('3');
dataInstance3.getData = () => [false, {'e': 3}];
const dataInstance4 = new DummyData_('4');
dataInstance4.getData = () => [undefined, 0];
// Note that dataInstance2 is not saved.
mr.PersistentDataManager.saveData(dataInstance1);
mr.PersistentDataManager.saveData(dataInstance3);
mr.PersistentDataManager.saveData(dataInstance4);
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
'd': 1
});
expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
'e': 1
});
expect(mr.PersistentDataManager.getTemporaryData(dataInstance2)).toBeNull();
expect(mr.PersistentDataManager.getPersistentData(dataInstance2))
.toBeNull();
expect(mr.PersistentDataManager.getTemporaryData(dataInstance3))
.toEqual(false);
expect(mr.PersistentDataManager.getPersistentData(dataInstance3)).toEqual({
'e': 3
});
expect(mr.PersistentDataManager.getTemporaryData(dataInstance4)).toBeNull();
expect(mr.PersistentDataManager.getPersistentData(dataInstance4))
.toEqual(0);
});
it('Test unregister', () => {
dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
dataInstance2.getData = () => [{'d': 2}, {'e': 2}];
const dataInstance3 = new DummyData_('3');
dataInstance3.getData = () => [{'d': 3}, {'e': 3}];
mr.PersistentDataManager.register(dataInstance3);
onSuspendListener();
expect(mr.PersistentDataManager.getTemporaryData(dataInstance3)).toEqual({
'd': 3
});
mr.PersistentDataManager.unregister(dataInstance3);
// Get data should not be called again.
dataInstance3.getData = () => {
fail();
};
onSuspendListener();
});
it('handles version change', () => {
dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
onSuspendListener();
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
'd': 1
});
expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
'e': 1
});
version = '1.1';
expect(mr.PersistentDataManager.isChromeReloaded(mrInstanceId)).toBe(false);
mr.PersistentDataManager.initialize(mrInstanceId);
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toBe(null);
expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
'e': 1
});
});
it('handles mrInstanceId change', () => {
dataInstance1.getData = () => [{'d': 1}, {'e': 1}];
onSuspendListener();
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toEqual({
'd': 1
});
expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
'e': 1
});
mrInstanceId = '321';
expect(mr.PersistentDataManager.isChromeReloaded(mrInstanceId)).toBe(true);
mr.PersistentDataManager.initialize(mrInstanceId);
expect(mr.PersistentDataManager.getTemporaryData(dataInstance1)).toBe(null);
expect(mr.PersistentDataManager.getPersistentData(dataInstance1)).toEqual({
'e': 1
});
});
describe('allows writes', () => {
it('of small values', () => {
mr.PersistentDataManager.write('mr.temp.Buckaroo', 'Bonzai');
expect(window.localStorage.getItem('mr.temp.Buckaroo')).toBe('Bonzai');
expect(mr.PersistentDataManager.charsUsed_).toBe(22);
});
it('of large values over quota', () => {
mr.PersistentDataManager.QUOTA_CHARS = 100;
[1, 2, 3, 4].forEach(index => {
mr.PersistentDataManager.write(
'mr.temp.' + index, 'Only the dead have seen the end of war.');
});
expect(mr.PersistentDataManager.charsUsed_).toBe(96);
// Normally this would go over QUOTA_CHARS, but clearing temporary
// values allows it to succeed.
mr.PersistentDataManager.write(
'mr.persistent.5', 'Courage is knowing what not to fear.');
expect(mr.PersistentDataManager.charsUsed_).toBe(51);
[1, 2, 3, 4].forEach(index => {
expect(window.localStorage.getItem('mr.temp.' + index)).toBeNull();
});
expect(window.localStorage.getItem('mr.persistent.5'))
.toBe('Courage is knowing what not to fear.');
});
});
describe('stores and reads Blobs', () => {
const dm = mr.PersistentDataManager;
const testKey = 'test';
const allUint16Values = [];
for (let i = 0; i < (1 << 16); ++i) {
allUint16Values.push(i);
}
// Takes an array of byte values, creates a Blob, stores and retrieves it
// back, and then maps the bytes of the Blob back to an array of byte
// values.
function writeThenReadByteValues(values) {
return new Promise((resolve, reject) => {
dm.writeBlob(testKey, new Blob([new Uint8Array(values)])).then(() => {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.error) {
reject(reader.error);
} else {
resolve(Array.from(new Uint8Array(reader.result)));
}
};
reader.readAsArrayBuffer(dm.readBlob(testKey));
}, reject);
});
}
// Same as writeThenReadByteValues(), except operate on an array of uint16.
function writeThenReadShortValues(values) {
return new Promise((resolve, reject) => {
dm.writeBlob(testKey, new Blob([new Uint16Array(values)])).then(() => {
const reader = new FileReader();
reader.onloadend = () => {
if (reader.error) {
reject(reader.error);
} else {
resolve(Array.from(new Uint16Array(reader.result)));
}
};
reader.readAsArrayBuffer(dm.readBlob(testKey));
}, reject);
});
}
it('of zero size', done => {
writeThenReadByteValues([]).then(values => {
expect(values).toEqual([]);
done();
}, fail);
});
it('of even size', done => {
writeThenReadByteValues([42, 1]).then(values => {
expect(values).toEqual([42, 1]);
done();
}, fail);
});
it('of odd size', done => {
writeThenReadByteValues([42, 1, 0]).then(values => {
expect(values).toEqual([42, 1, 0]);
done();
}, fail);
});
it('containing all possible uint16 values', done => {
const original = allUint16Values.concat(allUint16Values.reverse());
writeThenReadShortValues(original).then(values => {
expect(values).toEqual(original);
done();
}, fail);
});
it('that are just within quota', done => {
// Note on quota space needed: The string length of the key plus value
// must be available. A blob of 7 uint16 values will have a byte size of
// 14. The impl will add two padding bytes to the end, making the total 16
// bytes. Thus, 16/2 = 8 chars of quota must be available for the value.
// In total, 4 + 8 (key + value) chars must be available.
mr.PersistentDataManager.QUOTA_CHARS =
mr.PersistentDataManager.charsUsed_ + testKey.length + 8;
const original = allUint16Values.slice(0, 7);
writeThenReadShortValues(original).then(values => {
expect(values).toEqual(original);
done();
}, fail);
});
it('that exceed quota', done => {
// See note above on how quota accounting works.
mr.PersistentDataManager.QUOTA_CHARS =
mr.PersistentDataManager.charsUsed_ + testKey.length + 8;
const original = allUint16Values.slice(0, 8);
writeThenReadShortValues(original).then(fail, error => {
expect(dm.readBlob(testKey)).toBeNull();
done();
});
});
});
});