diff --git a/src/shared/util.js b/src/shared/util.js index 8fbb1b812c8..cc27940c0f5 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -17,6 +17,23 @@ export function isTrue (v: any): boolean %checks { export function isFalse (v: any): boolean %checks { return v === false } + +/** + * Sorts an object by key to ensure seraialized equality + * by succinct key order. Guaranteed to at least return an + * empty object. + */ +export function keySort (obj: any): Object { + if (isObject(obj) === false) { + return {} + } + const sorted = {} + Object.keys(obj).sort().forEach(key => { + sorted[key] = obj[key] + }) + return sorted +} + /** * Check if value is primitive */ @@ -240,7 +257,7 @@ export function looseEqual (a: mixed, b: mixed): boolean { const isObjectB = isObject(b) if (isObjectA && isObjectB) { try { - return JSON.stringify(a) === JSON.stringify(b) + return JSON.stringify(keySort(a)) === JSON.stringify(keySort(b)) } catch (e) { // possible circular reference return a === b diff --git a/test/unit/modules/shared/key-sort.spec.js b/test/unit/modules/shared/key-sort.spec.js new file mode 100644 index 00000000000..ec865fa098e --- /dev/null +++ b/test/unit/modules/shared/key-sort.spec.js @@ -0,0 +1,55 @@ +import { keySort, looseEqual } from 'shared/util' + +describe('keySort', () => { + const chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()_+./' + const randomWord = () => { + const letters = [] + for (let cnt = 0; cnt < 10; cnt++) { + letters.push(chars.charAt(Math.floor(Math.random() * chars.length))) + } + return letters.join('') + } + const unsortedObject = () => { + const unsorted = {} + chars.split('').map(c => { + unsorted[c + '' + randomWord()] = randomWord() + }) + return unsorted + } + + it('returns Object for empty Object argument', () => { + expect(looseEqual(keySort({}), {})).toBe(true) + }) + + it('returns Object for Array argument', () => { + expect(looseEqual(keySort([]), {})).toBe(true) + }) + + it('returns Object for String argument', () => { + expect(looseEqual(keySort('Test String'), {})).toBe(true) + }) + + it('returns Object for Number argument', () => { + expect(looseEqual(keySort(Number.MAX_SAFE_INTEGER), {})).toBe(true) + }) + + it('returns Object for Undefined argument', () => { + expect(looseEqual(keySort(undefined), {})).toBe(true) + }) + + it('returns Object for Null argument', () => { + expect(looseEqual(keySort(null), {})).toBe(true) + }) + + it('Sorts an unsorted Object, which are equal', () => { + const unsortedTestObj = unsortedObject() + const sortedObject = keySort(unsortedTestObj) + expect(looseEqual(sortedObject, unsortedTestObj)).toBe(true) + }) + + it('does not unsort a sorted object', () => { + const sortedObject = keySort(unsortedObject()) + const sortedObjectCompare = keySort(sortedObject) + expect(looseEqual(sortedObject, sortedObjectCompare)).toBe(true) + }) +})