aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json1
-rw-r--r--ext/mixed/js/core.js66
-rw-r--r--test/test-core.js132
3 files changed, 198 insertions, 1 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index 2f8306f7..3723d97d 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -110,6 +110,7 @@
"escapeRegExp": "readonly",
"deferPromise": "readonly",
"clone": "readonly",
+ "deepEqual": "readonly",
"generateId": "readonly",
"promiseAnimationFrame": "readonly",
"DynamicProperty": "readonly",
diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js
index 81330893..72ab7474 100644
--- a/ext/mixed/js/core.js
+++ b/ext/mixed/js/core.js
@@ -176,6 +176,72 @@ const clone = (() => {
return clone;
})();
+const deepEqual = (() => {
+ // eslint-disable-next-line no-shadow
+ function deepEqual(value1, value2) {
+ if (value1 === value2) { return true; }
+
+ const type = typeof value1;
+ if (typeof value2 !== type) { return false; }
+
+ switch (type) {
+ case 'object':
+ case 'function':
+ return deepEqualInternal(value1, value2, new Set());
+ default:
+ return false;
+ }
+ }
+
+ function deepEqualInternal(value1, value2, visited1) {
+ if (value1 === value2) { return true; }
+
+ const type = typeof value1;
+ if (typeof value2 !== type) { return false; }
+
+ switch (type) {
+ case 'object':
+ case 'function':
+ {
+ if (value1 === null || value2 === null) { return false; }
+ const array = Array.isArray(value1);
+ if (array !== Array.isArray(value2)) { return false; }
+ if (visited1.has(value1)) { return false; }
+ visited1.add(value1);
+ return array ? areArraysEqual(value1, value2, visited1) : areObjectsEqual(value1, value2, visited1);
+ }
+ default:
+ return false;
+ }
+ }
+
+ function areObjectsEqual(value1, value2, visited1) {
+ const keys1 = Object.keys(value1);
+ const keys2 = Object.keys(value2);
+ if (keys1.length !== keys2.length) { return false; }
+
+ const keys1Set = new Set(keys1);
+ for (const key of keys2) {
+ if (!keys1Set.has(key) || !deepEqualInternal(value1[key], value2[key], visited1)) { return false; }
+ }
+
+ return true;
+ }
+
+ function areArraysEqual(value1, value2, visited1) {
+ const length = value1.length;
+ if (length !== value2.length) { return false; }
+
+ for (let i = 0; i < length; ++i) {
+ if (!deepEqualInternal(value1[i], value2[i], visited1)) { return false; }
+ }
+
+ return true;
+ }
+
+ return deepEqual;
+})();
+
function generateId(length) {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
diff --git a/test/test-core.js b/test/test-core.js
index 3e8b8414..2b29b7f1 100644
--- a/test/test-core.js
+++ b/test/test-core.js
@@ -32,7 +32,7 @@ const vm = new VM({
vm.execute([
'mixed/js/core.js'
]);
-const [DynamicProperty] = vm.get(['DynamicProperty']);
+const [DynamicProperty, deepEqual] = vm.get(['DynamicProperty', 'deepEqual']);
function testDynamicProperty() {
@@ -161,9 +161,139 @@ function testDynamicProperty() {
}
}
+function testDeepEqual() {
+ const data = [
+ // Simple tests
+ {
+ value1: 0,
+ value2: 0,
+ expected: true
+ },
+ {
+ value1: null,
+ value2: null,
+ expected: true
+ },
+ {
+ value1: 'test',
+ value2: 'test',
+ expected: true
+ },
+ {
+ value1: true,
+ value2: true,
+ expected: true
+ },
+ {
+ value1: 0,
+ value2: 1,
+ expected: false
+ },
+ {
+ value1: null,
+ value2: false,
+ expected: false
+ },
+ {
+ value1: 'test1',
+ value2: 'test2',
+ expected: false
+ },
+ {
+ value1: true,
+ value2: false,
+ expected: false
+ },
+
+ // Simple object tests
+ {
+ value1: {},
+ value2: {},
+ expected: true
+ },
+ {
+ value1: {},
+ value2: [],
+ expected: false
+ },
+ {
+ value1: [],
+ value2: [],
+ expected: true
+ },
+ {
+ value1: {},
+ value2: null,
+ expected: false
+ },
+
+ // Complex object tests
+ {
+ value1: [1],
+ value2: [],
+ expected: false
+ },
+ {
+ value1: [1],
+ value2: [1],
+ expected: true
+ },
+ {
+ value1: [1],
+ value2: [2],
+ expected: false
+ },
+
+ {
+ value1: {},
+ value2: {test: 1},
+ expected: false
+ },
+ {
+ value1: {test: 1},
+ value2: {test: 1},
+ expected: true
+ },
+ {
+ value1: {test: 1},
+ value2: {test: {test2: false}},
+ expected: false
+ },
+ {
+ value1: {test: {test2: true}},
+ value2: {test: {test2: false}},
+ expected: false
+ },
+ {
+ value1: {test: {test2: [true]}},
+ value2: {test: {test2: [true]}},
+ expected: true
+ },
+
+ // Recursive
+ {
+ value1: (() => { const x = {}; x.x = x; return x; })(),
+ value2: (() => { const x = {}; x.x = x; return x; })(),
+ expected: false
+ }
+ ];
+
+ let index = 0;
+ for (const {value1, value2, expected} of data) {
+ const actual1 = deepEqual(value1, value2);
+ assert.strictEqual(actual1, expected, `Failed for test ${index}`);
+
+ const actual2 = deepEqual(value2, value1);
+ assert.strictEqual(actual2, expected, `Failed for test ${index}`);
+
+ ++index;
+ }
+}
+
function main() {
testDynamicProperty();
+ testDeepEqual();
}