Dynamic property (#749)

* Add DynamicProperty class

* Add tests for DynamicProperty
This commit is contained in:
toasted-nutbread 2020-08-22 17:50:56 -04:00 committed by GitHub
parent f0c974d319
commit a96e1c20a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 275 additions and 2 deletions

View File

@ -108,6 +108,7 @@
"deferPromise": "readonly",
"clone": "readonly",
"generateId": "readonly",
"DynamicProperty": "readonly",
"EventDispatcher": "readonly",
"EventListenerCollection": "readonly"
}

View File

@ -260,7 +260,7 @@ function promiseTimeout(delay, resolveValue) {
/*
* Common events
* Common classes
*/
class EventDispatcher {
@ -348,3 +348,106 @@ class EventListenerCollection {
this._eventListeners = [];
}
}
/**
* Class representing a generic value with an override stack.
* Changes can be observed by listening to the 'change' event.
*/
class DynamicProperty extends EventDispatcher {
/**
* Creates a new instance with the specified value.
* @param value The value to assign.
*/
constructor(value) {
super();
this._value = value;
this._defaultValue = value;
this._overrides = [];
}
/**
* Gets the default value for the property, which is assigned to the
* public value property when no overrides are present.
*/
get defaultValue() {
return this._defaultValue;
}
/**
* Assigns the default value for the property. If no overrides are present
* and if the value is different than the current default value,
* the 'change' event will be triggered.
* @param value The value to assign.
*/
set defaultValue(value) {
this._defaultValue = value;
if (this._overrides.length === 0) { this._updateValue(); }
}
/**
* Gets the current value for the property, taking any overrides into account.
*/
get value() {
return this._value;
}
/**
* Gets the number of overrides added to the property.
*/
get overrideCount() {
return this._overrides.length;
}
/**
* Adds an override value with the specified priority to the override stack.
* Values with higher priority will take precedence over those with lower.
* For tie breaks, the override value added first will take precedence.
* If the newly added override has the highest priority of all overrides
* and if the override value is different from the current value,
* the 'change' event will be fired.
* @param value The override value to assign.
* @param priority The priority value to use, as a number.
* @returns A string token which can be passed to the clearOverride function
* to remove the override.
*/
setOverride(value, priority=0) {
const overridesCount = this._overrides.length;
let i = 0;
for (; i < overridesCount; ++i) {
if (priority > this._overrides[i].priority) { break; }
}
const token = generateId(16);
this._overrides.splice(i, 0, {value, priority, token});
if (i === 0) { this._updateValue(); }
return token;
}
/**
* Removes a specific override value. If the removed override
* had the highest priority, and the new value is different from
* the previous value, the 'change' event will be fired.
* @param token The token for the corresponding override which is to be removed.
* @returns true if an override was returned, false otherwise.
*/
clearOverride(token) {
for (let i = 0, ii = this._overrides.length; i < ii; ++i) {
if (this._overrides[i].token === token) {
this._overrides.splice(i, 1);
if (i === 0) { this._updateValue(); }
return true;
}
}
return false;
}
/**
* Updates the current value using the current overrides and default value.
* If the new value differs from the previous value, the 'change' event will be fired.
*/
_updateValue() {
const value = this._overrides.length > 0 ? this._overrides[0].value : this._defaultValue;
if (this._value === value) { return; }
this._value = value;
this.trigger('change', {value});
}
}

View File

@ -9,7 +9,7 @@
"build": "node ./dev/build.js",
"test": "npm run test-lint && npm run test-code && npm run test-manifest",
"test-lint": "eslint . && node ./test/lint/global-declarations.js",
"test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document-util.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js && node ./test/test-dom-text-scanner.js && node ./test/test-cache-map.js && node ./test/test-profile-conditions.js",
"test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document-util.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js && node ./test/test-dom-text-scanner.js && node ./test/test-cache-map.js && node ./test/test-profile-conditions.js && node ./test/test-core.js",
"test-manifest": "node ./test/test-manifest.js"
},
"repository": {

169
test/test-core.js Normal file
View File

@ -0,0 +1,169 @@
/*
* Copyright (C) 2020 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const assert = require('assert');
const crypto = require('crypto');
const {VM} = require('./yomichan-vm');
const vm = new VM({
crypto: {
getRandomValues: (array) => {
const buffer = crypto.randomBytes(array.byteLength);
buffer.copy(array);
return array;
}
}
});
vm.execute([
'mixed/js/core.js'
]);
const [DynamicProperty] = vm.get(['DynamicProperty']);
function testDynamicProperty() {
const data = [
{
initialValue: 0,
operations: [
{
operation: null,
expectedDefaultValue: 0,
expectedValue: 0,
expectedOverrideCount: 0,
expeectedEventOccurred: false
},
{
operation: 'set.defaultValue',
args: [1],
expectedDefaultValue: 1,
expectedValue: 1,
expectedOverrideCount: 0,
expeectedEventOccurred: true
},
{
operation: 'set.defaultValue',
args: [1],
expectedDefaultValue: 1,
expectedValue: 1,
expectedOverrideCount: 0,
expeectedEventOccurred: false
},
{
operation: 'set.defaultValue',
args: [0],
expectedDefaultValue: 0,
expectedValue: 0,
expectedOverrideCount: 0,
expeectedEventOccurred: true
},
{
operation: 'setOverride',
args: [8],
expectedDefaultValue: 0,
expectedValue: 8,
expectedOverrideCount: 1,
expeectedEventOccurred: true
},
{
operation: 'setOverride',
args: [16],
expectedDefaultValue: 0,
expectedValue: 8,
expectedOverrideCount: 2,
expeectedEventOccurred: false
},
{
operation: 'setOverride',
args: [32, 1],
expectedDefaultValue: 0,
expectedValue: 32,
expectedOverrideCount: 3,
expeectedEventOccurred: true
},
{
operation: 'setOverride',
args: [64, -1],
expectedDefaultValue: 0,
expectedValue: 32,
expectedOverrideCount: 4,
expeectedEventOccurred: false
},
{
operation: 'clearOverride',
args: [-4],
expectedDefaultValue: 0,
expectedValue: 32,
expectedOverrideCount: 3,
expeectedEventOccurred: false
},
{
operation: 'clearOverride',
args: [-3],
expectedDefaultValue: 0,
expectedValue: 32,
expectedOverrideCount: 2,
expeectedEventOccurred: false
},
{
operation: 'clearOverride',
args: [-2],
expectedDefaultValue: 0,
expectedValue: 64,
expectedOverrideCount: 1,
expeectedEventOccurred: true
},
{
operation: 'clearOverride',
args: [-1],
expectedDefaultValue: 0,
expectedValue: 0,
expectedOverrideCount: 0,
expeectedEventOccurred: true
}
]
}
];
for (const {initialValue, operations} of data) {
const property = new DynamicProperty(initialValue);
const overrideTokens = [];
let eventOccurred = false;
const onChange = () => { eventOccurred = true; };
property.on('change', onChange);
for (const {operation, args, expectedDefaultValue, expectedValue, expectedOverrideCount, expeectedEventOccurred} of operations) {
eventOccurred = false;
switch (operation) {
case 'set.defaultValue': property.defaultValue = args[0]; break;
case 'setOverride': overrideTokens.push(property.setOverride(...args)); break;
case 'clearOverride': property.clearOverride(overrideTokens[overrideTokens.length + args[0]]); break;
}
assert.strictEqual(eventOccurred, expeectedEventOccurred);
assert.strictEqual(property.defaultValue, expectedDefaultValue);
assert.strictEqual(property.value, expectedValue);
assert.strictEqual(property.overrideCount, expectedOverrideCount);
}
property.off('change', onChange);
}
}
function main() {
testDynamicProperty();
}
if (require.main === module) { main(); }