Skip to content

Commit ff46cbc

Browse files
authored
fix(mock-doc): fully svg compliant (#1739)
1 parent 5f2fc80 commit ff46cbc

File tree

8 files changed

+152
-24
lines changed

8 files changed

+152
-24
lines changed

src/mock-doc/element.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ export function createElement(ownerDocument: any, tagName: string) {
4949
return new MockCanvasElement(ownerDocument);
5050
}
5151

52-
if (SVG_TAGS.has(tagName)) {
53-
return new MockSVGElement(ownerDocument, tagName);
54-
}
55-
5652
if (ownerDocument != null && tagName.includes('-')) {
5753
const win = ownerDocument.defaultView;
5854
if (win != null && win.customElements != null) {
@@ -73,9 +69,6 @@ export function createElementNS(ownerDocument: any, namespaceURI: string, tagNam
7369
}
7470
}
7571

76-
// This set is intentionally incomplete. More tags can be added as needed.
77-
const SVG_TAGS = new Set(['circle', 'line', 'g', 'path', 'svg', 'symbol', 'viewbox']);
78-
7972
class MockAnchorElement extends MockHTMLElement {
8073
constructor(ownerDocument: any) {
8174
super(ownerDocument, 'a');
@@ -215,7 +208,7 @@ patchPropAttributes(MockScriptElement.prototype, {
215208
type: String
216209
});
217210

218-
class MockSVGElement extends MockElement {
211+
export class MockSVGElement extends MockElement {
219212
// SVGElement properties and methods
220213
get ownerSVGElement(): SVGSVGElement { return null; }
221214
get viewportElement(): SVGElement { return null; }

src/mock-doc/node.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export class MockElement extends MockNode {
186186
super(
187187
ownerDocument,
188188
NODE_TYPES.ELEMENT_NODE,
189-
typeof nodeName === 'string' ? nodeName.toUpperCase() : null,
189+
typeof nodeName === 'string' ? nodeName : null,
190190
null
191191
);
192192
this.namespaceURI = null;
@@ -542,7 +542,7 @@ export class MockElement extends MockNode {
542542
set tabIndex(value: number) { this.setAttributeNS(null, 'tabindex', value); }
543543

544544
get tagName() { return this.nodeName; }
545-
set tagName(value: string) { this.nodeName = value.toUpperCase(); }
545+
set tagName(value: string) { this.nodeName = value; }
546546

547547
get textContent() {
548548
const text: string[] = [];
@@ -679,6 +679,19 @@ function insertBefore(parentNode: MockNode, newNode: MockNode, referenceNode: Mo
679679
}
680680

681681
export class MockHTMLElement extends MockElement {
682+
683+
namespaceURI = 'http://www.w3.org/1999/xhtml';
684+
685+
constructor(ownerDocument: any, nodeName: string) {
686+
super(
687+
ownerDocument,
688+
typeof nodeName === 'string' ? nodeName.toUpperCase() : null,
689+
);
690+
}
691+
692+
get tagName() { return this.nodeName; }
693+
set tagName(value: string) { this.nodeName = value; }
694+
682695
get attributes() {
683696
let attrs = attrsMap.get(this);
684697
if (attrs == null) {

src/mock-doc/parse-util.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ function getParser(ownerDocument: MockDocument) {
5757
},
5858

5959
createElement(tagName: string, namespaceURI: string, attrs: Attribute[]) {
60-
const elm = ownerDocument.createElement(tagName);
61-
elm.namespaceURI = namespaceURI;
62-
60+
const elm = ownerDocument.createElementNS(namespaceURI, tagName);
6361
for (let i = 0; i < attrs.length; i++) {
6462
const attr = attrs[i];
6563

@@ -175,7 +173,11 @@ function getParser(ownerDocument: MockDocument) {
175173
},
176174

177175
getTagName(element: MockElement) {
178-
return element.nodeName.toLowerCase();
176+
if (element.namespaceURI === 'http://www.w3.org/1999/xhtml') {
177+
return element.nodeName.toLowerCase();
178+
} else {
179+
return element.nodeName;
180+
}
179181
},
180182

181183
getNamespaceURI(element: MockElement) {

src/mock-doc/serialize-node.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export function serializeNodeToHtml(elm: Node | MockNode, opts: SerializeNodeToH
7979

8080
function serializeToHtml(node: Node, opts: SerializeNodeToHtmlOptions, output: SerializeOutput, isShadowRoot: boolean) {
8181
if (node.nodeType === NODE_TYPES.ELEMENT_NODE || isShadowRoot) {
82-
const tagName = isShadowRoot ? 'mock:shadow-root' : node.nodeName.toLowerCase();
82+
const tagName = isShadowRoot ? 'mock:shadow-root' : getTagName(node as Element);
8383

8484
if (tagName === 'body') {
8585
output.isWithinBody = true;
@@ -402,6 +402,14 @@ const LT_REGEX = /</g;
402402
const GT_REGEX = />/g;
403403
const CAN_REMOVE_ATTR_QUOTES = /^[^ \t\n\f\r"'`=<>\/\\-]+$/;
404404

405+
function getTagName(element: Element) {
406+
if (element.namespaceURI === 'http://www.w3.org/1999/xhtml') {
407+
return element.nodeName.toLowerCase();
408+
} else {
409+
return element.nodeName;
410+
}
411+
}
412+
405413
function escapeString(str: string, attrMode: boolean) {
406414
str = str.replace(AMP_REGEX, '&amp;').replace(NBSP_REGEX, '&nbsp;');
407415

src/mock-doc/test/custom-elements.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('customElements', () => {
2626
}
2727
});
2828

29-
const cmpA = document.createElement('cmp-a');
29+
const cmpA = document.createElement('CMP-a');
3030
cmpA.setAttribute('attr-a', 'value-a');
3131
expect(attrName).toBe('attr-a');
3232
expect(oldValue).toBe(null);

src/mock-doc/test/element.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { MockDocument } from '../document';
22
import { MockWindow, cloneWindow } from '../window';
33
import { MockElement, MockHTMLElement } from '../node';
44
import { XLINK_NS } from '../../runtime/runtime-constants';
5+
import { MockSVGElement } from '../element';
56

67

78
describe('element', () => {
@@ -119,6 +120,69 @@ describe('element', () => {
119120
});
120121
});
121122

123+
describe('namespaceURI', () => {
124+
it('HTMLElement namespaceURI is always http://www.w3.org/1999/xhtml', () => {
125+
const htmlElement = new MockHTMLElement(doc, 'svg');
126+
expect(htmlElement.namespaceURI).toEqual('http://www.w3.org/1999/xhtml');
127+
128+
const createdElement1 = doc.createElement('div');
129+
expect(createdElement1.namespaceURI).toEqual('http://www.w3.org/1999/xhtml');
130+
131+
const createdElement2 = doc.createElement('svg');
132+
expect(createdElement2.namespaceURI).toEqual('http://www.w3.org/1999/xhtml');
133+
expect(createdElement2 instanceof MockHTMLElement).toBe(true);
134+
135+
const createdElement3 = doc.createElementNS('http://www.w3.org/1999/xhtml', 'svg');
136+
expect(createdElement3.namespaceURI).toEqual('http://www.w3.org/1999/xhtml');
137+
expect(createdElement3 instanceof MockHTMLElement).toBe(true);
138+
});
139+
140+
it('Element namespace is null by defualt', () => {
141+
const element = new MockElement(doc, 'svg');
142+
expect(element.namespaceURI).toEqual(null);
143+
});
144+
145+
it('createElementNS sets the namespace', () => {
146+
const element = doc.createElementNS('random', 'svg');
147+
expect(element.namespaceURI).toEqual('random');
148+
expect(element instanceof MockSVGElement).toBe(false);
149+
150+
const element1 = doc.createElementNS('http://www.w3.org/2000/svg', 'svg');
151+
expect(element1.namespaceURI).toEqual('http://www.w3.org/2000/svg');
152+
expect(element1 instanceof MockSVGElement).toBe(true);
153+
});
154+
});
155+
156+
describe('tagName', () => {
157+
it('Element tagName/nodeName is case sensible', () => {
158+
const element = new MockElement(doc, 'myElement');
159+
expect(element.tagName).toEqual('myElement');
160+
expect(element.nodeName).toEqual('myElement');
161+
162+
const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
163+
expect(foreignObject.tagName).toEqual('foreignObject');
164+
expect(foreignObject.nodeName).toEqual('foreignObject');
165+
});
166+
167+
it('HTMLElement tagName/nodeName is case insensible', () => {
168+
const element = new MockHTMLElement(doc, 'myElement');
169+
expect(element.tagName).toEqual('MYELEMENT');
170+
expect(element.nodeName).toEqual('MYELEMENT');
171+
172+
const foreignObject = document.createElement('foreignObject');
173+
expect(foreignObject.tagName).toEqual('FOREIGNOBJECT');
174+
expect(foreignObject.nodeName).toEqual('FOREIGNOBJECT');
175+
176+
const foreignObject2 = document.createElementNS('http://www.w3.org/1999/xhtml', 'foreignObject');
177+
expect(foreignObject2.tagName).toEqual('FOREIGNOBJECT');
178+
expect(foreignObject2.nodeName).toEqual('FOREIGNOBJECT');
179+
180+
const createdElement = document.createElement('myElement');
181+
expect(createdElement.tagName).toEqual('MYELEMENT');
182+
expect(createdElement.nodeName).toEqual('MYELEMENT');
183+
});
184+
});
185+
122186
describe('attributes', () => {
123187
it('attributes are case sensible in Element', () => {
124188
const element = new MockElement(doc, 'div');

src/mock-doc/test/html-parse.spec.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,45 @@ describe('parseHtml', () => {
4141
expect(article.tagName).toBe('CMP-A');
4242
});
4343

44+
it('svg', () => {
45+
doc = new MockDocument(`
46+
<div>
47+
<svg>
48+
<a>Hello</a>
49+
<feImage></feImage>
50+
<foreignObject>
51+
<a>Hello</a>
52+
<feImage></feImage>
53+
</foreignObject>
54+
<svg>
55+
</div>
56+
`);
57+
expect(doc.body.firstElementChild.tagName).toEqual('DIV');
58+
expect(doc.body.firstElementChild.firstElementChild.tagName).toEqual('svg');
59+
expect(doc.body.firstElementChild.firstElementChild.children[0].tagName).toEqual('a');
60+
expect(doc.body.firstElementChild.firstElementChild.children[1].tagName).toEqual('feImage');
61+
expect(doc.body.firstElementChild.firstElementChild.children[2].tagName).toEqual('foreignObject');
62+
expect(doc.body.firstElementChild.firstElementChild.children[2].children[0].tagName).toEqual('A');
63+
expect(doc.body.firstElementChild.firstElementChild.children[2].children[1].tagName).toEqual('FEIMAGE');
64+
expect(doc.body).toEqualHtml(`
65+
<div>
66+
<svg>
67+
<a>
68+
Hello
69+
</a>
70+
<feImage></feImage>
71+
<foreignObject>
72+
<a>
73+
Hello
74+
</a>
75+
<feimage></feimage>
76+
</foreignObject>
77+
<svg></svg>
78+
</svg>
79+
</div>
80+
`);
81+
});
82+
4483
it('template', () => {
4584
doc = new MockDocument(`
4685
<template>text</template>
@@ -215,7 +254,7 @@ describe('parseHtml', () => {
215254

216255
it('should mock canvas api', () => {
217256
const elm = parseHtmlToFragment('<canvas id="canvas" width="300" height="300"></canvas>');
218-
const canvas = elm.children[0];
257+
const canvas = elm.children[0];
219258
const ctx: CanvasRenderingContext2D = canvas.getContext('2d');
220259
expect(elm.children.length).toBe(1);
221260
expect(elm.children[0].nodeName).toBe('CANVAS');

src/runtime/vdom/test/patch-svg.spec.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('renderer', () => {
1818
describe('created element', () => {
1919

2020
it('has tag', () => {
21-
patch(vnode0, h('div', null), document);
21+
patch(vnode0, h('div', null));
2222
expect(hostElm.tagName).toEqual('DIV');
2323
});
2424

@@ -29,10 +29,19 @@ describe('renderer', () => {
2929
h('foreignObject', null,
3030
h('div', null, 'I am HTML embedded in SVG')
3131
)
32-
), document);
32+
));
3333

3434
expect(svgElm.firstChild.namespaceURI).toEqual(SVG_NS);
3535
expect(svgElm.firstChild.firstChild.namespaceURI).not.toEqual(SVG_NS);
36+
expect(svgElm).toEqualHtml(`
37+
<svg>
38+
<foreignObject>
39+
<div>
40+
I am HTML embedded in SVG
41+
</div>
42+
</foreignObject>
43+
</svg>`
44+
);
3645
});
3746

3847
it('should not affect subsequence element', () => {
@@ -42,11 +51,11 @@ describe('renderer', () => {
4251
h('circle', null)
4352
] as any),
4453
h('div', null)
45-
] as any), document);
54+
] as any));
4655

4756
expect(hostElm.tagName).toEqual('DIV');
4857
expect(hostElm.namespaceURI).not.toEqual(SVG_NS);
49-
expect(hostElm.firstElementChild.tagName).toEqual('SVG');
58+
expect(hostElm.firstElementChild.tagName).toEqual('svg');
5059
expect(hostElm.firstElementChild.namespaceURI).toEqual(SVG_NS);
5160
expect(hostElm.firstElementChild.firstChild.namespaceURI).toEqual(SVG_NS);
5261
expect(hostElm.firstElementChild.lastChild.namespaceURI).toEqual(SVG_NS);
@@ -62,7 +71,7 @@ describe('renderer', () => {
6271
h('div', null,
6372
h('svg', null)
6473
)
65-
), document);
74+
));
6675

6776
const vnode1 = toVNode(vnode0.$elm$);
6877

@@ -72,11 +81,11 @@ describe('renderer', () => {
7281
),
7382
h('div', null)
7483
] as any
75-
), document);
84+
));
7685

7786
const vnode2 = toVNode(vnode1.$elm$);
7887
expect(vnode2.$children$[0].$elm$.tagName).toEqual('DIV');
79-
expect(vnode2.$children$[0].$children$[0].$elm$.tagName).toEqual('SVG');
88+
expect(vnode2.$children$[0].$children$[0].$elm$.tagName).toEqual('svg');
8089
expect(vnode2.$children$[1].$elm$.tagName).toEqual('DIV');
8190
});
8291
});

0 commit comments

Comments
 (0)