Skip to content

Commit

Permalink
chore: [#1079] Continues on implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Nov 1, 2024
1 parent 9ee4fcb commit 150bc7d
Show file tree
Hide file tree
Showing 19 changed files with 246 additions and 183 deletions.
2 changes: 1 addition & 1 deletion packages/happy-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"test": "vitest run",
"test:ui": "vitest --ui",
"test:watch": "vitest",
"test:debug": "vitest run --inspect-brk --threads=false"
"test:debug": "vitest run --inspect-brk --no-file-parallelism"
},
"dependencies": {
"entities": "^4.5.0",
Expand Down
112 changes: 49 additions & 63 deletions packages/happy-dom/src/ClassMethodBinder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,79 +2,65 @@
* Node utility.
*/
export default class ClassMethodBinder {
private target: Object;
private classes: any[];
private cache = new Map<string | symbol, Boolean>();

/**
* Binds methods to a target.
* Constructor.
*
* @param target Target.
* @param classes Classes.
* @param [options] Options.
* @param [options.bindSymbols] Bind symbol methods.
* @param [options.forwardToPrototype] Forwards the method calls to the prototype. This makes it possible for test tools to override methods on the prototype (e.g. Object.defineProperty(HTMLCollection.prototype, 'item', {})).
* @param [options.proxy] Bind methods using a proxy.
*/
public static bindMethods(
target: Object,
classes: any[],
options?: { bindSymbols?: boolean; forwardToPrototype?: boolean; proxy?: any }
): void {
for (const _class of classes) {
const propertyDescriptors = Object.getOwnPropertyDescriptors(_class.prototype);
const keys: Array<string | symbol> = Object.keys(propertyDescriptors);
constructor(target: Object, classes: any[]) {
this.target = target;
this.classes = classes;
}

if (options?.bindSymbols) {
for (const symbol of Object.getOwnPropertySymbols(propertyDescriptors)) {
keys.push(symbol);
}
}
/**
* Binds method, getters and setters to a target.
*
* @param name Method name.
*/
public bind(name: string | symbol): void {
if (this.cache.has(name)) {
return;
}

const scope = options?.proxy ? options.proxy : target;
this.cache.set(name, true);

if (options?.forwardToPrototype) {
for (const key of keys) {
const descriptor = propertyDescriptors[<string>key];
if (descriptor.get || descriptor.set) {
Object.defineProperty(target, key, {
...descriptor,
get:
descriptor.get &&
(() => Object.getOwnPropertyDescriptor(_class.prototype, key).get.call(scope)),
set:
descriptor.set &&
((newValue) =>
Object.getOwnPropertyDescriptor(_class.prototype, key).set.call(scope, newValue))
});
} else if (
key !== 'constructor' &&
typeof descriptor.value === 'function' &&
!descriptor.value.toString().startsWith('class ')
) {
Object.defineProperty(target, key, {
...descriptor,
value: (...args) => _class.prototype[key].apply(scope, args)
});
}
}
} else {
for (const key of keys) {
const descriptor = propertyDescriptors[<string>key];
if (descriptor.get || descriptor.set) {
Object.defineProperty(target, key, {
...descriptor,
get: descriptor.get?.bind(scope),
set: descriptor.set?.bind(scope)
});
} else if (
key !== 'constructor' &&
typeof descriptor.value === 'function' &&
!descriptor.value.toString().startsWith('class ')
) {
Object.defineProperty(target, key, {
...descriptor,
value: descriptor.value.bind(scope)
});
}
const target = this.target;

if (!(name in target)) {
return;
}

for (const _class of this.classes) {
const descriptor = Object.getOwnPropertyDescriptor(_class.prototype, name);
if (descriptor) {
if (typeof descriptor.value === 'function') {
Object.defineProperty(target, name, {
...descriptor,
value: descriptor.value.bind(target)
});
} else if (descriptor.get !== undefined) {
Object.defineProperty(target, name, {
...descriptor,
get: descriptor.get?.bind(target),
set: descriptor.set?.bind(target)
});
}
return;
}
}
}

/**
* Prevents a method, getter or setter from being bound.
*
* @param name Method name.
*/
public preventBinding(name: string | symbol): void {
this.cache.set(name, true);
}
}
1 change: 1 addition & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,4 @@ export const invertSelf = Symbol('invertSelf');
export const getLength = Symbol('getLength');
export const currentScale = Symbol('currentScale');
export const rotate = Symbol('rotate');
export const bindMethods = Symbol('bindMethods');
11 changes: 6 additions & 5 deletions packages/happy-dom/src/dom/DOMTokenList.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ClassMethodBinder from '../ClassMethodBinder.js';
import Element from '../nodes/element/Element.js';
import * as PropertySymbol from '../PropertySymbol.js';
import ClassMethodBinder from '../ClassMethodBinder.js';

const ATTRIBUTE_SPLIT_REGEXP = /[\t\f\n\r ]+/;

Expand Down Expand Up @@ -35,17 +35,15 @@ export default class DOMTokenList {
this[PropertySymbol.ownerElement] = ownerElement;
this[PropertySymbol.attributeName] = attributeName;

ClassMethodBinder.bindMethods(this, [DOMTokenList], {
bindSymbols: true,
forwardToPrototype: true
});
const methodBinder = new ClassMethodBinder(this, [DOMTokenList]);

return new Proxy(this, {
get: (target, property) => {
if (property === 'length') {
return target[PropertySymbol.getTokenList]().length;
}
if (property in target || typeof property === 'symbol') {
methodBinder.bind(property);
return target[property];
}
const index = Number(property);
Expand All @@ -54,6 +52,7 @@ export default class DOMTokenList {
}
},
set(target, property, newValue): boolean {
methodBinder.bind(property);
if (typeof property === 'symbol') {
target[property] = newValue;
return true;
Expand Down Expand Up @@ -91,6 +90,8 @@ export default class DOMTokenList {
return !isNaN(index) && index >= 0 && index < target[PropertySymbol.getTokenList]().length;
},
defineProperty(target, property, descriptor): boolean {
methodBinder.preventBinding(property);

if (property in target) {
Object.defineProperty(target, property, descriptor);
return true;
Expand Down
11 changes: 6 additions & 5 deletions packages/happy-dom/src/nodes/element/HTMLCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ export default class HTMLCollection<T extends Element, NamedItem = T> {

this[PropertySymbol.query] = query;

// This only works for one level of inheritance, but it should be fine as there is no collection that goes deeper according to spec.
ClassMethodBinder.bindMethods(
const methodBinder = new ClassMethodBinder(
this,
this.constructor !== HTMLCollection ? [HTMLCollection, this.constructor] : [HTMLCollection],
{ bindSymbols: true }
this.constructor !== HTMLCollection ? [this.constructor, HTMLCollection] : [HTMLCollection]
);

return new Proxy(this, {
Expand All @@ -40,16 +38,17 @@ export default class HTMLCollection<T extends Element, NamedItem = T> {
return query().length;
}
if (property in target || typeof property === 'symbol') {
methodBinder.bind(property);
return target[property];
}

const index = Number(property);
if (!isNaN(index)) {
return query()[index];
}
return target.namedItem(<string>property) || undefined;
},
set(target, property, newValue): boolean {
methodBinder.bind(property);
if (typeof property === 'symbol') {
target[property] = newValue;
return true;
Expand Down Expand Up @@ -123,6 +122,8 @@ export default class HTMLCollection<T extends Element, NamedItem = T> {
return false;
},
defineProperty(target, property, descriptor): boolean {
methodBinder.preventBinding(property);

if (property in target) {
Object.defineProperty(target, property, descriptor);
return true;
Expand Down
4 changes: 0 additions & 4 deletions packages/happy-dom/src/nodes/element/NamedNodeMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import DOMException from '../../exception/DOMException.js';
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
import Element from './Element.js';
import NamespaceURI from '../../config/NamespaceURI.js';
import ClassMethodBinder from '../../ClassMethodBinder.js';

/**
* Named Node Map.
Expand All @@ -29,9 +28,6 @@ export default class NamedNodeMap {
*/
constructor(ownerElement: Element) {
this[PropertySymbol.ownerElement] = ownerElement;
ClassMethodBinder.bindMethods(this, [NamedNodeMap], {
bindSymbols: true
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable filenames/match-exported */

import ClassMethodBinder from '../../ClassMethodBinder.js';
import * as PropertySymbol from '../../PropertySymbol.js';
import NamedNodeMap from './NamedNodeMap.js';

Expand All @@ -18,12 +19,15 @@ export default class NamedNodeMapProxyFactory {
const namedItems = namedNodeMap[PropertySymbol.namedItems];
const namespaceItems = namedNodeMap[PropertySymbol.namespaceItems];

const methodBinder = new ClassMethodBinder(this, [NamedNodeMap]);

return new Proxy<NamedNodeMap>(namedNodeMap, {
get: (target, property) => {
if (property === 'length') {
return namespaceItems.size;
}
if (property in target || typeof property === 'symbol') {
methodBinder.bind(property);
return target[property];
}
const index = Number(property);
Expand All @@ -33,6 +37,7 @@ export default class NamedNodeMapProxyFactory {
return target.getNamedItem(<string>property) || undefined;
},
set(target, property, newValue): boolean {
methodBinder.bind(property);
if (typeof property === 'symbol') {
target[property] = newValue;
return true;
Expand Down Expand Up @@ -79,6 +84,8 @@ export default class NamedNodeMapProxyFactory {
return false;
},
defineProperty(target, property, descriptor): boolean {
methodBinder.preventBinding(property);

if (property in target) {
Object.defineProperty(target, property, descriptor);
return true;
Expand Down
25 changes: 14 additions & 11 deletions packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import BrowserWindow from '../../window/BrowserWindow.js';
import THTMLFormControlElement from './THTMLFormControlElement.js';
import QuerySelector from '../../query-selector/QuerySelector.js';
import RadioNodeList from './RadioNodeList.js';
import WindowBrowserContext from '../../window/WindowBrowserContext.js';
import ClassMethodBinder from '../../ClassMethodBinder.js';
import Node from '../node/Node.js';
import Element from '../element/Element.js';
import EventTarget from '../../event/EventTarget.js';
import Node from '../node/Node.js';
import ClassMethodBinder from '../../ClassMethodBinder.js';
import WindowBrowserContext from '../../window/WindowBrowserContext.js';

/**
* HTML Form Element.
Expand All @@ -43,21 +43,21 @@ export default class HTMLFormElement extends HTMLElement {
constructor() {
super();

ClassMethodBinder.bindMethods(
this,
[EventTarget, Node, Element, HTMLElement, HTMLFormElement],
{
bindSymbols: true,
forwardToPrototype: true
}
);
const methodBinder = new ClassMethodBinder(this, [
HTMLFormElement,
HTMLElement,
Element,
Node,
EventTarget
]);

const proxy = new Proxy(this, {
get: (target, property) => {
if (property === 'length') {
return target[PropertySymbol.getFormControlItems]().length;
}
if (property in target || typeof property === 'symbol') {
methodBinder.bind(property);
return target[property];
}
const index = Number(property);
Expand All @@ -67,6 +67,7 @@ export default class HTMLFormElement extends HTMLElement {
return target[PropertySymbol.getFormControlNamedItem](<string>property) || undefined;
},
set(target, property, newValue): boolean {
methodBinder.bind(property);
if (typeof property === 'symbol') {
target[property] = newValue;
return true;
Expand Down Expand Up @@ -127,6 +128,8 @@ export default class HTMLFormElement extends HTMLElement {
return false;
},
defineProperty(target, property, descriptor): boolean {
methodBinder.preventBinding(property);

if (!descriptor.value) {
Object.defineProperty(target, property, descriptor);
return true;
Expand Down
10 changes: 5 additions & 5 deletions packages/happy-dom/src/nodes/html-media-element/TextTrackList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,15 @@ export default class TextTrackList extends EventTarget {

this[PropertySymbol.items] = items;

// This only works for one level of inheritance, but it should be fine as there is no collection that goes deeper according to spec.
ClassMethodBinder.bindMethods(this, [TextTrackList], {
bindSymbols: true,
forwardToPrototype: true
});
const methodBinder = new ClassMethodBinder(this, [TextTrackList, EventTarget]);

return new Proxy(this, {
get: (target, property) => {
if (property === 'length') {
return items.length;
}
if (property in target || typeof property === 'symbol') {
methodBinder.bind(property);
return target[property];
}
const index = Number(property);
Expand All @@ -52,6 +49,7 @@ export default class TextTrackList extends EventTarget {
}
},
set(target, property, newValue): boolean {
methodBinder.bind(property);
if (typeof property === 'symbol') {
target[property] = newValue;
return true;
Expand Down Expand Up @@ -90,6 +88,8 @@ export default class TextTrackList extends EventTarget {
return !isNaN(index) && index >= 0 && index < items.length;
},
defineProperty(target, property, descriptor): boolean {
methodBinder.preventBinding(property);

if (property in target) {
Object.defineProperty(target, property, descriptor);
return true;
Expand Down
Loading

0 comments on commit 150bc7d

Please sign in to comment.