Dynamic property (#749)
* Add DynamicProperty class * Add tests for DynamicProperty
This commit is contained in:
parent
f0c974d319
commit
a96e1c20a7
@ -108,6 +108,7 @@
|
|||||||
"deferPromise": "readonly",
|
"deferPromise": "readonly",
|
||||||
"clone": "readonly",
|
"clone": "readonly",
|
||||||
"generateId": "readonly",
|
"generateId": "readonly",
|
||||||
|
"DynamicProperty": "readonly",
|
||||||
"EventDispatcher": "readonly",
|
"EventDispatcher": "readonly",
|
||||||
"EventListenerCollection": "readonly"
|
"EventListenerCollection": "readonly"
|
||||||
}
|
}
|
||||||
|
@ -260,7 +260,7 @@ function promiseTimeout(delay, resolveValue) {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Common events
|
* Common classes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class EventDispatcher {
|
class EventDispatcher {
|
||||||
@ -348,3 +348,106 @@ class EventListenerCollection {
|
|||||||
this._eventListeners = [];
|
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"build": "node ./dev/build.js",
|
"build": "node ./dev/build.js",
|
||||||
"test": "npm run test-lint && npm run test-code && npm run test-manifest",
|
"test": "npm run test-lint && npm run test-code && npm run test-manifest",
|
||||||
"test-lint": "eslint . && node ./test/lint/global-declarations.js",
|
"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"
|
"test-manifest": "node ./test/test-manifest.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
169
test/test-core.js
Normal file
169
test/test-core.js
Normal 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(); }
|
Loading…
Reference in New Issue
Block a user