Skip to content

Commit

Permalink
fix(utils/clone): don't try to clone elements from different window c…
Browse files Browse the repository at this point in the history
…ontext (#4072)

* fix(utils/clone): don't try to clone elements from different window context

* weakmap

* don't use cache between calls

* use strict equal

* use strict equal

* cache

* create on internal

* typo
  • Loading branch information
straker committed Jun 30, 2023
1 parent de3da89 commit 55000d0
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 46 deletions.
58 changes: 35 additions & 23 deletions lib/core/utils/clone.js
@@ -1,35 +1,47 @@
/**
* Deeply clones an object or array
* Deeply clones an object or array. DOM nodes or collections of DOM nodes are not deeply cloned and are instead returned as is.
* @param {Mixed} obj The object/array to clone
* @return {Mixed} A clone of the initial object or array
* @return {Mixed} A clone of the initial object or array
*/
function clone(obj) {
/* eslint guard-for-in: 0*/
var index,
length,
out = obj;
// DOM nodes cannot be cloned.
export default function clone(obj) {
return cloneRecused(obj, new Map());
}

// internal function to hide non-user facing parameters
function cloneRecused(obj, seen) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

// don't clone DOM nodes. since we can pass nodes from different window contexts
// we'll also use duck typing to determine what is a DOM node
if (
(window?.Node && obj instanceof window.Node) ||
(window?.HTMLCollection && obj instanceof window.HTMLCollection)
(window?.HTMLCollection && obj instanceof window.HTMLCollection) ||
('nodeName' in obj && 'nodeType' in obj && 'ownerDocument' in obj)
) {
return obj;
}

if (obj !== null && typeof obj === 'object') {
if (Array.isArray(obj)) {
out = [];
for (index = 0, length = obj.length; index < length; index++) {
out[index] = clone(obj[index]);
}
} else {
out = {};
for (index in obj) {
out[index] = clone(obj[index]);
}
}
// handle circular references by caching the cloned object and returning it
if (seen.has(obj)) {
return seen.get(obj);
}

if (Array.isArray(obj)) {
const out = [];
seen.set(obj, out);
obj.forEach(value => {
out.push(cloneRecused(value, seen));
});
return out;
}

const out = {};
seen.set(obj, out);
// eslint-disable-next-line guard-for-in
for (const key in obj) {
out[key] = cloneRecused(obj[key], seen);
}
return out;
}

export default clone;
108 changes: 85 additions & 23 deletions test/core/utils/clone.js
@@ -1,26 +1,26 @@
describe('utils.clone', function () {
'use strict';
var clone = axe.utils.clone;
describe('utils.clone', () => {
const clone = axe.utils.clone;
const fixture = document.querySelector('#fixture');

it('should clone an object', function () {
var obj = {
it('should clone an object', () => {
const obj = {
cats: true,
dogs: 2,
fish: [0, 1, 2]
fish: [0, 1, { one: 'two' }]
};
var c = clone(obj);
const c = clone(obj);

obj.cats = false;
obj.dogs = 1;
obj.fish[0] = 'stuff';
obj.fish[2].one = 'three';

assert.strictEqual(c.cats, true);
assert.strictEqual(c.dogs, 2);
assert.deepEqual(c.fish, [0, 1, 2]);
assert.deepEqual(c.fish, [0, 1, { one: 'two' }]);
});

it('should clone nested objects', function () {
var obj = {
it('should clone nested objects', () => {
const obj = {
cats: {
fred: 1,
billy: 2,
Expand All @@ -33,7 +33,7 @@ describe('utils.clone', function () {
},
fish: [0, 1, 2]
};
var c = clone(obj);
const c = clone(obj);

obj.cats.fred = 47;
obj.dogs = 47;
Expand All @@ -54,45 +54,107 @@ describe('utils.clone', function () {
assert.deepEqual(c.fish, [0, 1, 2]);
});

it('should clone objects with methods', function () {
var obj = {
cats: function () {
it('should clone objects with methods', () => {
const obj = {
cats: () => {
return 'meow';
},
dogs: function () {
dogs: () => {
return 'woof';
}
};
var c = clone(obj);
const c = clone(obj);

assert.strictEqual(obj.cats, c.cats);
assert.strictEqual(obj.dogs, c.dogs);

obj.cats = function () {};
obj.dogs = function () {};
obj.cats = () => {};
obj.dogs = () => {};

assert.notStrictEqual(obj.cats, c.cats);
assert.notStrictEqual(obj.dogs, c.dogs);
});

it('should clone prototypes', function () {
it('should clone prototypes', () => {
function Cat(name) {
this.name = name;
}

Cat.prototype.meow = function () {
Cat.prototype.meow = () => {
return 'meow';
};

Cat.prototype.bark = function () {
Cat.prototype.bark = () => {
return 'cats dont bark';
};

var cat = new Cat('Fred'),
const cat = new Cat('Fred'),
c = clone(cat);

assert.deepEqual(cat.name, c.name);
assert.deepEqual(Cat.prototype.bark, c.bark);
assert.deepEqual(Cat.prototype.meow, c.meow);
});

it('should clone circular objects while keeping the circular reference', () => {
const obj = { cats: true };
obj.child = obj;
const c = clone(obj);

obj.cats = false;

assert.deepEqual(c, {
cats: true,
child: c
});
assert.strictEqual(c, c.child);
});

it('should not return the same object when cloned twice', () => {
const obj = { cats: true };
const c1 = clone(obj);
const c2 = clone(obj);

assert.notStrictEqual(c1, c2);
});

it('should not return the same object when nested', () => {
const obj = { dogs: true };
const obj1 = { cats: true, child: { prop: obj } };
const obj2 = { fish: [0, 1, 2], child: { prop: obj } };

const c1 = clone(obj1);
const c2 = clone(obj2);

assert.notStrictEqual(c1.child.prop, c2.child.prop);
});

it('should not clone HTML elements', () => {
const obj = {
cats: true,
node: document.createElement('div')
};
const c = clone(obj);

obj.cats = false;

assert.equal(c.cats, true);
assert.strictEqual(c.node, obj.node);
});

it('should not clone HTML elements from different windows', () => {
fixture.innerHTML = '<iframe id="target"></iframe>';
const iframe = fixture.querySelector('#target');

const obj = {
cats: true,
node: iframe.contentDocument
};
const c = clone(obj);

obj.cats = false;

assert.equal(c.cats, true);
assert.strictEqual(c.node, obj.node);
});
});

0 comments on commit 55000d0

Please sign in to comment.