Skip to content

Commit 8f3bbe1

Browse files
committed
#1176@patch: Custom Elements letter case handling.
1 parent fb72997 commit 8f3bbe1

File tree

6 files changed

+77
-41
lines changed

6 files changed

+77
-41
lines changed

packages/happy-dom/src/custom-element/CustomElementRegistry.ts

+25-26
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ export default class CustomElementRegistry {
1212
/**
1313
* Validates the correctness of custom element tag names.
1414
*
15-
* @param tagName custom element tag name.
15+
* @param localName custom element tag name.
1616
* @returns boolean True, if tag name is standard compliant.
1717
*/
18-
private isValidCustomElementName(tagName: string): boolean {
18+
private isValidCustomElementName(localName: string): boolean {
1919
// Validation criteria based on:
2020
// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
2121
const PCENChar =
@@ -27,7 +27,7 @@ export default class CustomElementRegistry {
2727

2828
const PCEN = new RegExp(`^[a-z](${PCENChar})*-(${PCENChar})*$`, 'u');
2929

30-
const forbiddenNames = [
30+
const reservedNames = [
3131
'annotation-xml',
3232
'color-profile',
3333
'font-face',
@@ -37,33 +37,31 @@ export default class CustomElementRegistry {
3737
'font-face-name',
3838
'missing-glyph'
3939
];
40-
return PCEN.test(tagName) && !forbiddenNames.includes(tagName);
40+
return PCEN.test(localName) && !reservedNames.includes(localName);
4141
}
4242

4343
/**
4444
* Defines a custom element class.
4545
*
46-
* @param tagName Tag name of element.
46+
* @param localName Tag name of element.
4747
* @param elementClass Element class.
4848
* @param [options] Options.
4949
* @param options.extends
5050
*/
5151
public define(
52-
tagName: string,
52+
localName: string,
5353
elementClass: typeof HTMLElement,
5454
options?: { extends: string }
5555
): void {
56-
const upperTagName = tagName.toUpperCase();
57-
58-
if (!upperTagName.includes('-')) {
56+
if (!this.isValidCustomElementName(localName)) {
5957
throw new DOMException(
6058
"Failed to execute 'define' on 'CustomElementRegistry': \"" +
61-
tagName +
59+
localName +
6260
'" is not a valid custom element name.'
6361
);
6462
}
6563

66-
this._registry[upperTagName] = {
64+
this._registry[localName] = {
6765
elementClass,
6866
extends: options && options.extends ? options.extends.toLowerCase() : null
6967
};
@@ -73,9 +71,9 @@ export default class CustomElementRegistry {
7371
elementClass._observedAttributes = elementClass.observedAttributes;
7472
}
7573

76-
if (this._callbacks[upperTagName]) {
77-
const callbacks = this._callbacks[upperTagName];
78-
delete this._callbacks[upperTagName];
74+
if (this._callbacks[localName]) {
75+
const callbacks = this._callbacks[localName];
76+
delete this._callbacks[localName];
7977
for (const callback of callbacks) {
8078
callback();
8179
}
@@ -85,12 +83,11 @@ export default class CustomElementRegistry {
8583
/**
8684
* Returns a defined element class.
8785
*
88-
* @param tagName Tag name of element.
86+
* @param localName Tag name of element.
8987
* @param HTMLElement Class defined.
9088
*/
91-
public get(tagName: string): typeof HTMLElement {
92-
const upperTagName = tagName.toUpperCase();
93-
return this._registry[upperTagName] ? this._registry[upperTagName].elementClass : undefined;
89+
public get(localName: string): typeof HTMLElement {
90+
return this._registry[localName] ? this._registry[localName].elementClass : undefined;
9491
}
9592

9693
/**
@@ -107,17 +104,19 @@ export default class CustomElementRegistry {
107104
/**
108105
* When defined.
109106
*
110-
* @param tagName Tag name of element.
107+
* @param localName Tag name of element.
111108
* @returns Promise.
112109
*/
113-
public whenDefined(tagName: string): Promise<void> {
114-
const upperTagName = tagName.toUpperCase();
115-
if (this.get(upperTagName)) {
110+
public whenDefined(localName: string): Promise<void> {
111+
if (!this.isValidCustomElementName(localName)) {
112+
return Promise.reject(new DOMException(`Invalid custom element name: "${localName}"`));
113+
}
114+
if (this.get(localName)) {
116115
return Promise.resolve();
117116
}
118117
return new Promise((resolve) => {
119-
this._callbacks[upperTagName] = this._callbacks[upperTagName] || [];
120-
this._callbacks[upperTagName].push(resolve);
118+
this._callbacks[localName] = this._callbacks[localName] || [];
119+
this._callbacks[localName].push(resolve);
121120
});
122121
}
123122

@@ -128,9 +127,9 @@ export default class CustomElementRegistry {
128127
* @returns First found Tag name or `null`.
129128
*/
130129
public getName(elementClass: typeof HTMLElement): string | null {
131-
const tagName = Object.keys(this._registry).find(
130+
const localName = Object.keys(this._registry).find(
132131
(k) => this._registry[k].elementClass === elementClass
133132
);
134-
return !!tagName ? tagName : null;
133+
return !!localName ? localName : null;
135134
}
136135
}

packages/happy-dom/src/nodes/document/Document.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ export default class Document extends Node implements IDocument {
823823
if (this.defaultView && options && options.is) {
824824
customElementClass = this.defaultView.customElements.get(String(options.is));
825825
} else if (this.defaultView) {
826-
customElementClass = this.defaultView.customElements.get(tagName);
826+
customElementClass = this.defaultView.customElements.get(String(qualifiedName));
827827
}
828828

829829
const elementClass: typeof Element =

packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem
2323
* @param parentNode Parent node.
2424
*/
2525
public _connectToNode(parentNode: INode = null): void {
26-
const tagName = this.tagName;
26+
const localName = this.localName;
2727

2828
// This element can potentially be a custom element that has not been defined yet
2929
// Therefore we need to register a callback for when it is defined in CustomElementRegistry and replace it with the registered element (see #404)
30-
if (tagName.includes('-') && this.ownerDocument.defaultView.customElements._callbacks) {
30+
if (localName.includes('-') && this.ownerDocument.defaultView.customElements._callbacks) {
3131
const callbacks = this.ownerDocument.defaultView.customElements._callbacks;
3232

3333
if (parentNode && !this._customElementDefineCallback) {
3434
const callback = (): void => {
3535
if (this.parentNode) {
36-
const newElement = <HTMLElement>this.ownerDocument.createElement(tagName);
36+
const newElement = <HTMLElement>this.ownerDocument.createElement(localName);
3737
(<INodeList<INode>>newElement._childNodes) = this._childNodes;
3838
(<IHTMLCollection<IElement>>newElement._children) = this._children;
3939
(<boolean>newElement.isConnected) = this.isConnected;
@@ -82,16 +82,16 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem
8282
this._connectToNode(null);
8383
}
8484
};
85-
callbacks[tagName] = callbacks[tagName] || [];
86-
callbacks[tagName].push(callback);
85+
callbacks[localName] = callbacks[localName] || [];
86+
callbacks[localName].push(callback);
8787
this._customElementDefineCallback = callback;
88-
} else if (!parentNode && callbacks[tagName] && this._customElementDefineCallback) {
89-
const index = callbacks[tagName].indexOf(this._customElementDefineCallback);
88+
} else if (!parentNode && callbacks[localName] && this._customElementDefineCallback) {
89+
const index = callbacks[localName].indexOf(this._customElementDefineCallback);
9090
if (index !== -1) {
91-
callbacks[tagName].splice(index, 1);
91+
callbacks[localName].splice(index, 1);
9292
}
93-
if (!callbacks[tagName].length) {
94-
delete callbacks[tagName];
93+
if (!callbacks[localName].length) {
94+
delete callbacks[localName];
9595
}
9696
this._customElementDefineCallback = null;
9797
}

packages/happy-dom/src/xml-parser/XMLParser.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export default class XMLParser {
107107
// Start tag.
108108

109109
const tagName = match[1].toUpperCase();
110+
const localName = match[1];
110111

111112
// Some elements are not allowed to be nested (e.g. "<a><a></a></a>" is not allowed.).
112113
// Therefore we need to auto-close the tag, so that it become valid (e.g. "<a></a><a></a>").
@@ -130,7 +131,7 @@ export default class XMLParser {
130131
tagName === 'SVG'
131132
? NamespaceURI.svg
132133
: (<IElement>currentNode).namespaceURI || NamespaceURI.html;
133-
const newElement = document.createElementNS(namespaceURI, tagName);
134+
const newElement = document.createElementNS(namespaceURI, localName);
134135

135136
currentNode.appendChild(newElement);
136137
currentNode = newElement;

packages/happy-dom/test/custom-element/CustomElementRegistry.test.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import CustomElement from '../CustomElement.js';
22
import CustomElementRegistry from '../../src/custom-element/CustomElementRegistry.js';
3+
import IWindow from '../../src/window/IWindow.js';
4+
import IDocument from '../../src/nodes/document/IDocument.js';
5+
import Window from '../../src/window/Window.js';
36
import { beforeEach, describe, it, expect } from 'vitest';
7+
import { rejects } from 'assert';
48

59
describe('CustomElementRegistry', () => {
610
let customElements;
11+
let window: IWindow;
12+
let document: IDocument;
713

814
beforeEach(() => {
15+
window = new Window();
16+
document = window.document;
917
customElements = new CustomElementRegistry();
1018
CustomElement.observedAttributesCallCount = 0;
1119
});
@@ -21,6 +29,7 @@ describe('CustomElementRegistry', () => {
2129
expect(customElements.isValidCustomElementName('a-\u00d9')).toBe(true);
2230
expect(customElements.isValidCustomElementName('a_b.c-d')).toBe(true);
2331
expect(customElements.isValidCustomElementName('font-face')).toBe(false);
32+
expect(customElements.isValidCustomElementName('a-Öa')).toBe(true);
2433
});
2534
});
2635

@@ -35,7 +44,7 @@ describe('CustomElementRegistry', () => {
3544
extends: 'ul'
3645
});
3746
expect(customElements.get('custom-element')).toBe(CustomElement);
38-
expect(customElements._registry['CUSTOM-ELEMENT'].extends).toBe('ul');
47+
expect(customElements._registry['custom-element'].extends).toBe('ul');
3948
});
4049

4150
it('Throws an error if tag name does not contain "-".', () => {
@@ -54,6 +63,11 @@ describe('CustomElementRegistry', () => {
5463
expect(CustomElement.observedAttributesCallCount).toBe(1);
5564
expect(CustomElement._observedAttributes).toEqual(['key1', 'key2']);
5665
});
66+
67+
it('Non-ASCII capital letter in localName.', () => {
68+
customElements.define('a-Öa', CustomElement);
69+
expect(customElements.get('a-Öa')).toBe(CustomElement);
70+
});
5771
});
5872

5973
describe('get()', () => {
@@ -65,9 +79,19 @@ describe('CustomElementRegistry', () => {
6579
it('Returns undefined if the tag name has not been defined.', () => {
6680
expect(customElements.get('custom-element')).toBe(undefined);
6781
});
82+
83+
it('Case sensitivity of get().', () => {
84+
customElements.define('custom-element', CustomElement);
85+
expect(customElements.get('CUSTOM-ELEMENT')).toBe(undefined);
86+
});
6887
});
6988

7089
describe('whenDefined()', () => {
90+
it('Throws an error if tag name looks invalide', async () => {
91+
const tagName = 'element';
92+
expect(async() => await customElements.whenDefined(tagName)).rejects.toThrow();
93+
});
94+
7195
it('Returns a promise which is fulfilled when an element is defined.', async () => {
7296
await new Promise((resolve) => {
7397
customElements.whenDefined('custom-element').then(resolve);
@@ -90,7 +114,19 @@ describe('CustomElementRegistry', () => {
90114

91115
it('Returns Tag name if element class is found in registry', () => {
92116
customElements.define('custom-element', CustomElement);
93-
expect(customElements.getName(CustomElement)).toMatch(/custom-element/i);
117+
expect(customElements.getName(CustomElement)).toMatch('custom-element');
118+
});
119+
});
120+
121+
describe('createElement()', () => {
122+
it('Case insensitive access via document.createElement()', () => {
123+
customElements.define('custom-element', CustomElement);
124+
expect(document.createElement('CUSTOM-ELEMENT').localName).toBe('custom-element');
125+
});
126+
127+
it('Non-ASCII capital letters in document.createElement()', () => {
128+
customElements.define('a-Öa', CustomElement);
129+
expect(document.createElement('a-Öa').localName).toMatch(/a-Öa/i);
94130
});
95131
});
96132
});

packages/happy-dom/test/nodes/html-unknown-element/HTMLUnknownElement.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('HTMLUnknownElement', () => {
2222

2323
parent.appendChild(element);
2424

25-
expect(window.customElements._callbacks['CUSTOM-ELEMENT'].length).toBe(1);
25+
expect(window.customElements._callbacks['custom-element'].length).toBe(1);
2626

2727
parent.removeChild(element);
2828

0 commit comments

Comments
 (0)