Skip to content

Commit

Permalink
fix: [#1627] Fixes bug where the reference to the parent in HTMLFormE…
Browse files Browse the repository at this point in the history
…lement remove, replaceWith, before, after, append, prepend, replaceChildren and insertAdjacentElement (#1652)
  • Loading branch information
capricorn86 authored Dec 31, 2024
1 parent 758e9ff commit 675e306
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 9 deletions.
38 changes: 29 additions & 9 deletions packages/happy-dom/src/nodes/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,14 +471,20 @@ export default class Node extends EventTarget {
* @returns Appended node.
*/
public [PropertySymbol.appendChild](node: Node, disableValidations = false): Node {
if (node[PropertySymbol.proxy]) {
node = node[PropertySymbol.proxy];
}

const self = this[PropertySymbol.proxy] || this;

if (!disableValidations) {
if (node === this) {
if (node === self) {
throw new this[PropertySymbol.window].DOMException(
"Failed to execute 'appendChild' on 'Node': Not possible to append a node as a child of itself."
);
}

if (NodeUtility.isInclusiveAncestor(node, this, true)) {
if (NodeUtility.isInclusiveAncestor(node, self, true)) {
throw new this[PropertySymbol.window].DOMException(
"Failed to execute 'appendChild' on 'Node': The new node is a parent of the node to insert to.",
DOMExceptionNameEnum.domException
Expand All @@ -501,7 +507,7 @@ export default class Node extends EventTarget {
node[PropertySymbol.parentNode][PropertySymbol.removeChild](node);
}

node[PropertySymbol.parentNode] = this[PropertySymbol.proxy] || this;
node[PropertySymbol.parentNode] = self;

node[PropertySymbol.clearCache]();

Expand All @@ -522,7 +528,7 @@ export default class Node extends EventTarget {

this[PropertySymbol.reportMutation](
new MutationRecord({
target: this,
target: self,
type: MutationTypeEnum.childList,
addedNodes: [node]
})
Expand All @@ -538,6 +544,10 @@ export default class Node extends EventTarget {
* @returns Removed node.
*/
public [PropertySymbol.removeChild](node: Node): Node {
if (node[PropertySymbol.proxy]) {
node = node[PropertySymbol.proxy];
}

node[PropertySymbol.parentNode] = null;

node[PropertySymbol.clearCache]();
Expand Down Expand Up @@ -578,7 +588,7 @@ export default class Node extends EventTarget {

this[PropertySymbol.reportMutation](
new MutationRecord({
target: this,
target: this[PropertySymbol.proxy] || this,
type: MutationTypeEnum.childList,
removedNodes: [node]
})
Expand All @@ -600,18 +610,28 @@ export default class Node extends EventTarget {
referenceNode: Node | null,
disableValidations = false
): Node {
if (newNode[PropertySymbol.proxy]) {
newNode = newNode[PropertySymbol.proxy];
}

if (referenceNode && referenceNode[PropertySymbol.proxy]) {
referenceNode = referenceNode[PropertySymbol.proxy];
}

if (newNode === referenceNode) {
return newNode;
}

const self = this[PropertySymbol.proxy] || this;

if (!disableValidations) {
if (newNode === this) {
if (newNode === self) {
throw new this[PropertySymbol.window].DOMException(
"Failed to execute 'insertBefore' on 'Node': Not possible to insert a node as a child of itself."
);
}

if (NodeUtility.isInclusiveAncestor(newNode, this, true)) {
if (NodeUtility.isInclusiveAncestor(newNode, self, true)) {
throw new this[PropertySymbol.window].DOMException(
"Failed to execute 'insertBefore' on 'Node': The new node is a parent of the node to insert to.",
DOMExceptionNameEnum.domException
Expand Down Expand Up @@ -649,7 +669,7 @@ export default class Node extends EventTarget {
newNode[PropertySymbol.parentNode][PropertySymbol.removeChild](newNode);
}

newNode[PropertySymbol.parentNode] = this[PropertySymbol.proxy] || this;
newNode[PropertySymbol.parentNode] = self;

newNode[PropertySymbol.clearCache]();

Expand Down Expand Up @@ -687,7 +707,7 @@ export default class Node extends EventTarget {

this[PropertySymbol.reportMutation](
new MutationRecord({
target: this,
target: self,
type: MutationTypeEnum.childList,
addedNodes: [newNode]
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,102 @@ describe('HTMLFormElement', () => {
});
});

describe('remove()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><form>Foo</form></section>';

const form = <HTMLFormElement>document.querySelector('form');

form.remove();

expect(document.body.children[0].children.length).toBe(0);
});
});

describe('replaceWith()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><form>Foo</form></section>';

const form = <HTMLFormElement>document.querySelector('form');

form.replaceWith(document.createElement('div'));

expect(document.body.children[0].children[0].tagName).toBe('DIV');
});
});

describe('before()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><form>Foo</form></section>';

const form = <HTMLFormElement>document.querySelector('form');

form.before(document.createElement('div'));

expect(document.body.children[0].children[0].tagName).toBe('DIV');
});
});

describe('after()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><form>Foo</form></section>';

const form = <HTMLFormElement>document.querySelector('form');

form.after(document.createElement('div'));

expect(document.body.children[0].children[1].tagName).toBe('DIV');
});
});

describe('append()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><form>Foo</form></section>';

const form = <HTMLFormElement>document.querySelector('form');

form.append(document.createElement('div'));

expect(form.children[0].tagName).toBe('DIV');
});
});

describe('prepend()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><form>Foo</form></section>';

const form = <HTMLFormElement>document.querySelector('form');

form.prepend(document.createElement('div'));

expect(form.children[0].tagName).toBe('DIV');
});
});

describe('replaceChildren()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><form>Foo</form></section>';

const form = <HTMLFormElement>document.querySelector('form');

form.replaceChildren(document.createElement('div'));

expect(form.children[0].tagName).toBe('DIV');
});
});

describe('insertAdjacentElement()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><form>Foo</form></section>';

const form = <HTMLFormElement>document.querySelector('form');

form.insertAdjacentElement('beforebegin', document.createElement('div'));

expect(document.body.children[0].children[0].tagName).toBe('DIV');
});
});

for (const method of ['checkValidity', 'reportValidity']) {
describe(`${method}()`, () => {
it('Validates the form.', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,105 @@ describe('HTMLSelectElement', () => {
});
});

describe('remove()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><select><option>Option 1</option></select></section>';

const select = <HTMLSelectElement>document.querySelector('select');

select.remove();

expect(document.body.children[0].children.length).toBe(0);
});
});

describe('replaceWith()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><select><option>Option 1</option></select></section>';

const select = <HTMLSelectElement>document.querySelector('select');

select.replaceWith(document.createElement('div'));

expect(document.body.children[0].children[0].tagName).toBe('DIV');
});
});

describe('before()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><select><option>Option 1</option></select></section>';

const select = <HTMLSelectElement>document.querySelector('select');

select.before(document.createElement('div'));

expect(document.body.children[0].children[0].tagName).toBe('DIV');
});
});

describe('after()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><select><option>Option 1</option></select></section>';

const select = <HTMLSelectElement>document.querySelector('select');

select.after(document.createElement('div'));

expect(document.body.children[0].children[1].tagName).toBe('DIV');
});
});

describe('append()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><select><option>Option 1</option></select></section>';

const select = <HTMLSelectElement>document.querySelector('select');
const newOption = document.createElement('option');

select.append(newOption);

expect(select.children[1]).toBe(newOption);
});
});

describe('prepend()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><select><option>Option 1</option></select></section>';

const select = <HTMLSelectElement>document.querySelector('select');
const newOption = document.createElement('option');

select.prepend(newOption);

expect(select.children[0]).toBe(newOption);
});
});

describe('replaceChildren()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><select><option>Option 1</option></select></section>';

const select = <HTMLSelectElement>document.querySelector('select');
const newOption = document.createElement('option');

select.replaceChildren(newOption);

expect(select.children[0]).toBe(newOption);
});
});

describe('insertAdjacentElement()', () => {
it('Sets "parentNode" of child elements to the proxy and not the original element.', () => {
document.body.innerHTML = '<section><select><option>Option 1</option></select></section>';

const select = <HTMLSelectElement>document.querySelector('select');

select.insertAdjacentElement('beforebegin', document.createElement('div'));

expect(document.body.children[0].children[0].tagName).toBe('DIV');
});
});

describe('setCustomValidity()', () => {
it('Returns validation message.', () => {
element.setCustomValidity('Error message');
Expand Down

0 comments on commit 675e306

Please sign in to comment.