我们正在尝试使用 jsdom 对 React 核心进行单元测试(http://facebook.github.io/react/)。
不幸的是,jsdom 本身并不支持 web 组件规范,并且 webcomponents.js polyfill 不能在 jsdom 上运行。 此问题要求添加 WebComponent 支持(自定义元素、shadow dom、html 导入等)。
看看 webcomponents.js 使用了哪些 jsdom 不支持的 API 会很有趣。 如果我不得不猜测,那将比完整的 Web 组件规范更容易实现。
也就是说,实现 Web 组件会非常酷。 可能没有人们想象的那么难——规格相对较小。
刚刚有时间深入研究一下:
首先,我们没有在窗口范围内定义Window
。 我刚刚在Window
构造函数中用this.Window = this.prototype
修补了这个。
其次,webcomponentsjs 期望Window
有另一个原型,即EventTarget
原型,我们没有将其实现为单独的实体。
只是一点信息,因为我有一点时间。
好的。 应该能够很容易地暴露窗口。 EventTarget 原型有点棘手,但考虑到我们目前如何实现这些东西,它似乎是可行的; 这是我的 TODO。
好的,到目前为止的补丁相当简单:
this.Window = Window;
inherits(dom.EventTarget, Window, dom.EventTarget.prototype);
定义后的Windowwebcomponents.js 的下一次崩溃是由于我们没有实现HTMLUnknownElement
(#1068),在我们需要实现SVGUseElement
之后发生。 这就是我目前被阻止的内容,因为 webcomponents.js 显然不喜欢由 HTMLDivElement 填充的SVGUseElement
HTMLDivElement
并抛出一个断言。
好的,我再次检查了 Polyfill,我们需要实现/您需要填充以下内容:
HTMLUnknownElement
#1068SVGUseElement
window.CanvasRenderingContext2D
document.getRange()
、 window.getSelection()
、 window.Range
、 window.Selection
;#804 可能是一个开始)npm i canvas
(目前非详尽列表)
开始类似于以下内容:
jsdom.env({
file: __dirname + '/index.htm', // refers to webcomponent.js
created: function (err, window) {
jsdom.getVirtualConsole(window).sendTo(console)
window.document.createRange = function () { }
window.getSelection = function () { }
window.Range = function () { }
window.Selection = function () { }
window.CanvasRenderingContext2D = function () { } // Object.getPrototypeOf(require("canvas")(0,0).getContext("2d")) might be better
window.SVGUseElement = window.HTMLUnknownElement
},
done: function (err, window) {
console.log(err[0].data.error);
console.log(window.CustomElements)
},
features: {
ProcessExternalResources: ['script']
}
});
完成后,我们的HTMLDocument
构造函数中存在一些错误,这会导致最大调用堆栈错误。 构造函数目前仅供内部使用,但是站点上的某些脚本对其进行调用是有效的,因此我们需要使该构造函数可供公众使用。
+1 希望在 jsdom 上看到 WebComponents,特别是随着 Polymer 越来越受欢迎,能够在无头系统上测试自定义元素会很棒。
目前还没有 Web 组件的跨浏览器定义,因此实施还为时过早。 (我们不只是要复制 Chrome。)同时,您可以尝试将 Polymer 与 jsdom 一起使用。
@domenic足够公平。 好吧,它更多的是对我所追求的 WebComponents.js polyfill 的支持,因为这就是 Polymer 所依赖的 - 或webcomponents-lite
(除了 Shadow DOM 之外的所有这些 polyfill)。 做了一些尝试让 Polymer 在 jsdom 上工作,但到目前为止还没有运气——我假设上面评论中@Sebmaster的任务至少需要先修补。
我的理解是有问题的三个单独的 polyfill。 OP 中的那个与 Polymer 是分开的。 然后是 webcomponents.org polyfills,它曾经在 old-Polymer 中使用。 然后在 Polymer 1.0 中,我认为他们有自己的 polyfill,它们并不是真正的 polyfill,而是替代库,它们可以做一些类似于 web 组件的事情。 也许那是webcomponents-lite。
在 WebComponentsJS 存储库中,它说webcomponentsjs-lite
是一个变体,为所有 _but_ Shadow DOM 提供 polyfills,然后 Polymer 独立尝试使用他们的 Shady DOM 系统进行填充。 因此,我很确定 Polymer 尽可能多地依赖 WebComponents,而 WebComponentsJS polyfill 完成了繁重的工作。 lite 版本的重量应该大大减轻(很有趣..),所以我会看看我是否可以确定 jsdom 对 lite 版本的需求。 你认为让 polyfill(lite 或 full)在 jsdom 中工作的机会是多少?
如果不进行一些调查,真的很难说……期待您的发现。
是的,我认为我的待办事项列表仍然适用并且需要使用垫片。 合并 #1227 可能会使我们更快地实现符合标准的接口,因此我们可以更快地修复缺失的接口。
我(可能很天真)开始致力于添加 CustomElementsRegistry 作为了解 jsdom 结构的一种方式。 我在 web 平台测试列表中添加了“custom-elements/custom-elements-registry/define.html”,它在不应该通过的时候通过了(我还没有实现足够多)。 我很确定测试并没有真正运行,因为即使在测试顶部添加throw
也不会阻止它通过。 所以我显然错过了一些东西; 除了在test/web-platform-tests/index.js
中添加测试之外,我还需要做些什么吗?
似乎这是因为我们在初始const testWindow = iframe.contentDocument.defaultView;
行中失败了,因为contentDocument
由于某种原因未定义。 可能是我们的加载顺序与脚本执行的问题,但还没有深入研究。 希望能帮助你解决这个问题。 为了我们的目的,我们可能必须简化测试(现在)。
这很有帮助,谢谢! 我会看看我是否能弄清楚那里发生了什么,如果没有,我会按照你的建议创建一个简化的测试。
@Sebmaster以防万一您感兴趣,我对该测试的情况进行了一些研究,结果令我感到惊讶。
该测试使用html 的命名访问功能。 这意味着您可以执行以下操作:
<div id="foo"></div>
<script>
console.log(window.foo === document.getElementById('foo'));
</script>
_然而_,如果元素具有嵌套的浏览上下文,则全局应该指向它(参见链接的规范)。 对于 iframe,这就是contentWindow
。 jsdom 做对了,甚至还有一个 test 。 Safari 也做得对。
疯狂的是 Chrome 和 Firefox 搞错了。 全局指向 iframe,而不是 contentWindow。 看到这个,我认为这是一个 jsdom 错误并进行了一些搜索,最终找到了那个测试,这让我找到了规范。
tldr; 在 jsdom 上工作很有教育意义,你们做得很棒。
在各自的浏览器中提交错误。 还会向 web-platform-tests 发送 PR,我在测试中也发现了一些其他错误。
这对上游测试(如https://github.com/tmpvar/jsdom/blob/master/test/living-html/named-properties-window.js到 WPT)更有动力。 感谢您的发表! 这让我对 jsdom 感觉非常好^_^
你好!
我设法通过结合使 Custom Elements polyfill 与 jsdom 一起工作
注意:repo 使用 jsdom 8.5.0。 原因是我只在 MutationObserver polyfill 上取得了成功,它在内部使用了 Mutation Events。 由于性能不佳,突变事件在 8.5.0 之后被删除。 如果原生 Mutation Observer 出现,我将删除 polyfill 并更新到最新的 jsdom。
我有最新的 jsdom,并且https://github.com/WebReflection/document-register-element正在为我工作! 我一直在尝试更官方的 polyfill,但由于某种原因我遇到了麻烦。 我的目标是至少让自定义元素和 html 导入工作......如果我们能让 Polymer 也能工作,那就太棒了。
我可以让 Polymer 脚本正常运行。 我什至可以创建一个组件并将其传递给 Polymer 构造函数。 之后,它默默地失败了。 我认为影子 DOM 是问题所在。
我一直在尝试让 webcomponentsjs HTML 导入 polyfill 工作。 我可以让脚本运行,并且我相信我的 HTML 导入会执行 xmlhttprequest,但我的导入中的脚本似乎没有运行。
愿意分享一个例子@lastmjs? 我自己目前对 Web 组件非常感兴趣。 如果我能帮上忙,我很乐意为你做出贡献。
@snuggs谢谢! 给我一两天,我现在正处于一些紧迫的事情之中。
@snuggs如果我们可以让webcomponents-lite
polyfill 工作,我们应该能够使用 Polymer。 到目前为止,Shadow DOM 似乎是最难使用的 polyfill,如果我们使用webcomponents-lite
,我们暂时不必担心,因为我们可以访问template
、 custom elements
和HTML imports
。
我可以让 HTML 导入与webcomponents-lite
polyfill 一起使用。 我遇到了一些奇怪的行为,然后我遇到了这个: https ://github.com/Polymer/polymer/issues/1535 看起来 HTML 导入只能通过启用 cors 的非文件协议加载。 因此,我在项目的根目录中启动了一个快速 http 服务器:
npm install -g http-server
http-server --cors
这是我一直在使用的基本代码:
const jsdom = require('jsdom');
const doc = jsdom.jsdom(`
<!DOCTYPE html>
<html>
<head>
<script src="bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="http://localhost:8080/bower_components/polymer/polymer.html">
</head>
<body>
<test-app></test-app>
<dom-module id="test-app">
<template>
</template>
<script>
setTimeout(() => {
class TestApp {
beforeRegister() {
this.is = 'test-app';
console.log('before register');
}
ready() {
console.log('ready');
}
created() {
console.log('created');
}
attached() {
console.log('attached');
}
}
Polymer(TestApp);
}, 1000);
</script>
</dom-module>
</body>
</html>
`, {
virtualConsole: jsdom.createVirtualConsole().sendTo(console)
});
出于某种原因,我必须将TestApp
setTimeout
中。 看起来 Polymer HTML 导入并没有阻止 HTML 其余部分的呈现,因此如果没有setTimeout
,则不会定义 Polymer 构造函数。 这是 HTML 导入的正常行为吗?
beforeRegister
确实被调用了,所以 Polymer 构造函数正在做一些事情。 所以现在我们已经有效地导入了 HTML,当然模板也可以使用webcomponents-lite
polyfill。 我不确定自定义元素 polyfill 的表现如何。
当我在TestApp
类中放入ready
或created
方法时,它们不会被调用。 似乎没有正确处理生命周期事件。 该问题的根源可能在于自定义元素 polyfill 的实现。 我会继续玩。
需要解决的潜在问题:
更多的修补会带来更多的见解。 我认为进口和注册的顺序可能会让我们搞砸。 当我在调用 Polymer 构造函数后运行const testApp = document.createElement('test-app');
时,会调用created
和ready
方法,但不会调用attached
方法。 也许 jsdom 没有正确处理自定义元素文字? 此外,即使在调用document.body.appendChild(testApp)
时,也永远不会调用attached
生命周期方法。
这可能有助于理解加载顺序: https ://github.com/webcomponents/webcomponentsjs#helper -utilities
@lastmjs我目前一直在CustomElementRegistry.define()
和document.registerElement()
之间抛硬币。 我看到 Domenic 提供了一些很好的输入并合并了一些与 whatwg 相关的工作(https://github.com/whatwg/html/issues/1329) 似乎正在进行 API 迁移。 例如,我相信规范调用connectedCallback
与attachedCallback
功能配对。 还假设您说attached
attachedCallback
,因为该处理程序不是 API 的一部分。 我经历过define()
和registerElement()
针对每种方法触发不同的回调。 我已经弄清楚了自定义元素策略。 HTMLImports Domenic 在使用 XMLHTTPRequest 补丁的实现之前提到。 我相信可以直接从响应中直接转换为DocumentFragment
。 这样的气味可能是带有“进口”的蛇油。 “虚假”进口可能是理智所在。
在从 ES6 -> ES5 转换时, super()
似乎也被调用了 $#$9$# HTMLElement
,所以请注意这一点。 我一直在使用 Rollup.js/Babel 并被迫使用 webcomponents 包中的(轻量级)垫片。
https://developers.google.com/web/fundamentals/getting-started/primers/customelements
最后,当我使用原型标签创建时,我似乎获得了(更多)成功。
document.createElement('main', 'test-app')
正如@domenic之前向我提到的那样,我们要谨慎实施最低公分母规范,而不仅仅是做 GOOGLE 所做的事情。 似乎线条被网络组件模糊了。 但我是粉丝。
您一直在使用哪些方法?
到目前为止,我主要只使用webcomponents-lite
polyfills,并且 Polymer < 2.0。 因此,当我提到attached
方法时,我指的是他们使用的 Polymer 生命周期方法,而不是attachedCallback
。 另外,据我所知,polyfills 还没有切换到新的 v1 自定义元素规范。 所以我玩的所有东西都只是希望让 Polymer 与当前的 polyfill 一起工作。
@snuggs您现在是在使用 polyfill,还是在 jsdom 中进行实际实现?
@lastmjs我不使用 polyfill,因为我觉得没有必要完成 80% 的工作。 该平台现在已经足够成熟,只需稍加调整就可以使用本机构造。 我喜欢使用轻量级(通常是手卷)工具而不是框架。 也就是说,这不是大多数人。 似乎 Domenic 的意图是自定义元素 👍 html 导入 👎 但扩展 XMLHTTPRequest 以处理文档的fetching
没有问题,这将使我们到达那里。 那是大约 6 个月前的事了。 自实施以来发生了很大变化。 很有可能在想。 那么我们在哪里完成@lastmjs?
@snuggs也许最明智和面向未来的事情是在 jsdom 中实现对自定义元素和 Shadow DOM 的一流支持。 这两个标准都是 v1,据我所知,大多数主要浏览器似乎都将实施它们。 我们应该怎么做呢? 我现在时间有限,但也许我们至少可以列出需要做的事情。 @domenic您对如何推进这些实施有任何建议,或者我们不应该这样做的任何原因吗?
除了实施规范之外,我没有提出具体的建议:)
我有一个分支,前段时间我在其中工作(从那时起,规范发生了一些变化)。 实现 CustomElementsRegistry 很容易,我苦苦思索如何将自定义元素反应编织到代码库中,以及何时以及从何处调用这些反应。 如果我要选择这个备份(没有承诺),那可能就是我会关注的。
@matthewp这听起来很有帮助,我在哪里可以找到那个分支?
@matthewp是的,那会很好
https://github.com/matthewp/jsdom/commits/custom-elements就像我说的那样,从那时起规范发生了变化,所以它已经过时了。 这是最简单的部分,但如果有人想要它,它就是一个起点。 @snuggs @lastmjs
实际上,我使用 lib document-register-element来模拟 customElements
const {before} = require('mocha')
before(mockDOM)
before(mockCustomElements)
function mockDOM() {
const {JSDOM: Dom} = require('jsdom')
const dom = new Dom('<!doctype html><html><body></body></html>')
global.document = dom.window.document
global.window = document.defaultView
window.Object = Object
window.Math = Math
}
function mockCustomElements() {
require('document-register-element/pony')(window)
}
太棒了,你有什么问题吗?
直到现在,没有:D
但我需要写更多的规范,涵盖更多的东西让感觉更好
很高兴看到有一种方法。 尽管我很喜欢聚合物,但测试集是地狱,将 jsdom 作为后备是很好的 ;) 感谢您投入工作
看起来有一个 PR 推动了这一切! https://github.com/tmpvar/jsdom/pull/1872
实际上,我使用 lib document-register-element @darlanmendonca 模拟 customElements
应该阅读有关将 jsdom 全局变量附加到节点全局的链接。 这是一种反模式。
大家好,
我对在 JSDOM 中运行 Polymer 的状态有些困惑(使用 Node.js 6.7.0 和 JSDOM 11.1.0)。 我尝试了各种各样的东西,结果好坏参半。 如果有人能在这里填补我的空缺,我将不胜感激...
到目前为止我做了什么:
1) 我从我的根目录启动了一个 http 服务器
./node_modules/http-server/bin/http-server --cors
2)我将我的 Polymer 组件之一加载到 JSDOM 中:
jsdom.JSDOM.fromURL("http://localhost:8080/path/to/my-component.html",
{ runScripts: "dangerously",
resources: "usable"
})
.then(function (dom) {
setTimeout(() => {
window = dom.window;
component = window.document.querySelector("my-component");
}, 10000);
})
(我也尝试从文件系统加载组件文件,结果相同。)
3)这是我的组件代码:
<!DOCTYPE html>
<html>
<head>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
</head>
<body>
<link rel="import" href="/bower_components/polymer/polymer.html">
<dom-module id="order-app">
<template>
<h1>Hello Polymer</h1>
</template>
<script>
console.log("javascript is being executed");
addEventListener('WebComponentsReady', function () {
console.log("web components are ready");
Polymer({
is: 'order-app'
});
});
</script>
</dom-module>
</body>
</html>
(我添加了 HTML 头以加载 webcomponents polyfill。)
我能观察到什么?
当我运行它时,我看到
我看不到的
这使我得出结论,WebComponentsReady 事件没有被触发(可能是因为 HTML 导入不起作用?)。 还,
window.WebComponents
包含{ flags: { log: {} } }
- ready
指标缺失。
我还尝试了一些模拟和填充:
window.Object = Object;
window.Math = Math;
require('document-register-element/pony')(window);
但这似乎并没有改变任何东西。
现在,我想知道:-)这应该工作吗? 如果是这样,为什么它对我不起作用? 如果没有,让它工作缺少/需要什么?
感谢您的任何见解!
莫因
我什至尝试过,但成功率更低,我放弃等待这里讨论的结果。
https://github.com/sebs/noframework/blob/master/test/configurator.js
不是解决方案,只是另一个失败的尝试。 顺便说一句,同样的混乱。 同样的结论
在 jsdom 中填充自定义元素被证明是非常具有挑战性的。 有人可以列出在 jsdom 中实现它的挑战吗? 试图评估实现这一目标的努力程度。
根本的障碍是 jsdom共享构造函数和它们的原型。
这使得实现每个窗口的自定义元素注册表基本上是不可能的,因为HTMLElement
构造函数在所有窗口之间共享。 因此,当您在自定义元素构造函数中调用super()
时,正在运行的HTMLElement
构造函数不知道在哪个窗口中查找内容。这很糟糕。
我不确定是否有任何好的中间解决方案。 最重要的是将 jsdom 迁移到允许非共享构造函数/原型的架构。 我们可以通过几种方式做到这一点,所有这些都需要不同的权衡。 也许我们想打开一个专门的问题与团队和社区讨论它,但现在让我列出我脑海中浮现的那些:
[WebIDL2JSFactory]
,或者至少使用HTMLElement
及其所有后代。 我不确定[WebIDL2JSFactory]
是否还能很好地与继承一起工作,但它可以工作。 这种选择会导致每个人都支付额外的构造函数/原型的成本,但这也许比让自定义元素成为可选功能更好。vm
沙箱内的所有类定义模块。 例如,有一些构建步骤将 jsdom 中的所有 Web API 捆绑在一起,然后当您创建一个新窗口时,我们在新的全局沙箱中使用该捆绑包执行vm.runScript()
。 这可能会让我们摆脱[WebIDL2JSFactory]
。我想另一种解决方案是实现自定义元素,并带有一个巨大的警告,即自定义元素注册表是每个 Node.js 进程的全局? 但这似乎很可怕。
在最初的障碍之后,其余的就遵循规范而言相对简单。 最困难的部分可能是实现[CEReactions]
并更新我们所有的 IDL 文件以将其放在适当的位置,但这并不太难。
我也一直在考虑有一个单独的原型版本。 以下是我的一些想法。
我不确定
[WebIDL2JSFactory]
是否还能很好地与继承一起工作,但它可以工作。
不,它没有,我不确定如何让它工作。 在我看来,第二种解决方案更直接。
有一个选项,其中 jsdom 运行
vm
沙箱内的所有类定义模块。
这是我更喜欢的。 主要问题是在初始化期间将 impl 类传递到vm
沙箱,尽管这可以通过将外部上下文中的所有内容放入一个全局属性中来完成,并在完成后将delete
该全局属性. 它还允许正确实现[NamedConstructor]
和其他几个扩展属性,如果有人足够大胆,甚至可以为 jsdom 环境生成 V8 启动快照。
整个[WebIDL2JSFactory]
业务首先是一个黑客,我很想尽快摆脱它。
+1 评论对开发此功能没有帮助,因此我将删除至少一个最近的评论。
嗨,我没有意识到@TimothyGu正在研究这个。
我实际上有自定义元素注册和创建工作
https://github.com/mraerino/jsdom/tree/custom-elements-spec
我正在尝试尽可能减少侵入性,并尽可能接近规范。
自定义元素注册表 Web 平台测试正在通过。
昨晚在破解这个问题时,我发现了一个无需修改 webIdl2JS 即可工作的解决方案。
见这里: https ://github.com/mraerino/jsdom/commit/592ad1236e9ca8f63f789d48e1887003305bc618
@TimothyGu你愿意为此联合力量吗?
这里只是一些更新:
我对规范的实现非常有信心,但目前由于[HTMLConstructor]
扩展的 IDL 属性而陷入困境。 这就是我打开https://github.com/jsdom/webidl2js/issues/87的原因
同时,我将使用[Constructor]
属性实现[HTMLConstructor]
算法,以便以后能够轻松切换。 (我最初通过在window
中插入一个模拟 HTMLElement 类来实现它,但这似乎不正确。)
是的,正如https://github.com/tmpvar/jsdom/issues/1030#issuecomment -333994158 中所述,正确实现 HTMLConstructor 需要对 jsdom 的架构进行根本性的更改。
你有关于你的版本通过了多少网络平台测试的任何信息吗?
现在只是 customElementRegistry ,我的进度可能完全错误。
编辑:好的,在重新阅读您的评论后,我明白了您的意思。 我会用我的实现来试试,但@TimothyGu似乎也在进行分离。
我使用 Polymer,所以我是 :+1: 在这个请求功能上
@dman777 @mraerino同样适用于 slim.js 开发人员。 Slim 使用原生的 Web 组件 API,如果没有对 jsdom 的 hack,就无法继承 HTMLElement。
自从这个问题被打开以来已经过去了三年。 谁能说 jsdom 大约什么时候会支持自定义元素?
在过去的两周里,我一直在评估在 jsdom 中添加对自定义元素的支持的可行性。 这是调查的结果。
您可以在此处找到符合规范的自定义元素实现: https ://github.com/jsdom/jsdom/compare/master...pmdartus:custom-elements?expand=1。 这里和那里仍然有一些粗糙的边缘,但至少大部分WPT-test pass 。 剩下的失败测试要么是已知的 JSDOM 问题,要么是在我们处理 jsdom 中的实际实现时可以解决的小问题。
现在好消息是,Shadow DOM 已经得到原生支持,同时使用自定义元素分支和突变观察者,我能够在 jsdom 中加载和渲染最新版本的Polymer 3 hello world 应用程序示例🎉。 在当前状态下,分支无法加载 Stencil 应用程序(Stencil 开发模式需要一些不受支持的功能,例如module
,并且 prod 模式因未知原因抛出)。
这是之前需要首先进行的更改的列表,开始处理实际的自定义元素规范实现。 列表中的每个项目都是独立的,可以并行处理。
[CEReactions]
IDL 扩展属性jsdom 中缺少的用于添加对自定义元素的支持的核心功能之一是[CEReactions]
属性。 通过修补正确的原型属性,我部分能够解决这个问题。 只要自定义元素反应堆栈是全局的并且不是每个相关的相似来源浏览上下文的单元,这种方法就可以工作,因为所有接口原型都是共享的。
这种方法有一些缺点,因为某些接口具有与索引属性相关联的[CEReactions]
属性( HTMLOptionsCollection
, DOMStringMap
)。 在内部,jsdom 使用代理来跟踪这些属性的变化。 在这种情况下,接口的原型修补不起作用。 解决此问题的另一种方法是修补实现而不是接口(未实现)。
我对 webidl2js 内部不够熟悉,但我们应该探索为[CEReactions]
添加一个全局挂钩?
变化:
ce-reaction.js
:[HTMLConstructor]
IDL 扩展属性正如@domenic上面解释的那样,添加对[HTMLConstructor]
的支持是这里的主要障碍之一。
我可以通过修补每个浏览上下文的接口构造函数来解决这个问题。 接口构造函数将能够访问正确的窗口和文档对象,同时保持共享原型。 这种方法还避免了为每个新的浏览上下文重新评估界面原型的性能开销。
我不确定这是否是这里最好的方法,但它符合要求而不会引入额外的性能开销。
变化:
正如这里所讨论的,在Element.innerHTML
和Element.outerHTML
中使用的 HTML 片段解析算法实现是不正确的。 解析算法需要重构,才能正确调用自定义元素反应回调。
变化:
我很快偶然发现的问题之一是在添加对自定义元素创建的支持时引入了新的循环依赖项。 CustomElementRegistry 和创建元素算法都需要访问 Element 接口,从而创建循环依赖的噩梦。
分支中采用的方法是创建一个InterfaceCache
,这将允许通过元素命名空间和名称以及通过接口名称查找接口。 接口模块在评估后被延迟评估和缓存。 这种方法摆脱了循环依赖,因为接口不再需要在顶层。
这是解决 jsdom 中这个长期存在的问题的一种方法,这种方法的问题之一是它可能会破坏 jsdom 的 webpacked/browserified 版本(未经测试)。
变化:
Element.isConnected
以支持 Shadow DOM (https://github.com/jsdom/jsdom/pull/2424)~这是一个随着影子 DOM的引入而出现的问题,如果元素是影子树的一部分,isConnected 返回false
。 需要在此处添加新的 WPT 测试,因为没有测试检查此行为。
变化:
HTMLTemplateElement.templateContents
节点文档 (#2426)规范中定义的模板内容与 HTMLTemplateElement 本身具有不同的节点文档。 jsdom 今天没有实现这种行为,并且 HTMLTemplateElement 和它的模板内容共享相同
文档节点。
变化:
HTMLTemplateElement-impl.js
htmltodom.js
。 这种变化也对解析器有一些下游影响。 如果上下文元素是 HTMLTemplateElement,则 HTML 片段解析算法应该从模板内容而不是元素本身中提取文档节点。HTMLTemplateElement
(#2426)HTMLTemplateElement
在被另一个文档采用时需要运行一些特定的步骤。 据我所知,土接口有一个特殊的采用步骤。 采用节点算法实现也需要更新以调用此采用步骤。
变化:
序列化 HTML 片段算法,在序列化元素时查找与元素关联的值并将其反映为序列化内容中的属性。 在 parse5 树适配器中添加另一个钩子会很有趣,它将查找与元素getIsValue(element: Element): void | string
关联的 is 值。
如果元素具有关联的 is 值,则另一种方法(未实现)是添加更改当前getAttrList
挂钩的行为以将 is 值返回到属性列表。
在进行任何性能优化之前,我还想检查分支中更改的性能。 与树突变基准的 master 上的当前结果相比,添加自定义元素会增加 10% 的性能开销。 然而,与 master 相比,新 JSDOM 环境的创建现在慢了 3 到 6 倍,这需要更深入的调查来确定根本原因。
更多详情:这里
@pmdartus ,这是非常有前途的,出色的工作! 由于没有更好的选择,我一直在使用我的 hack 分支 jsdom-wc。 我看到一些奇怪的行为,并希望换成你的分支,但我遇到了问题。
我注册自定义元素,例如:
class Component extends HTMLElement {
}
customElements.define('custom-component', Component);
但如果我这样做:
const el = assign(this.fixture, {
innerHTML: `
<custom-component></custom-component>
`,
});
我立即得到: Error: Uncaught [TypeError: Illegal constructor]
。
对此有什么想法吗?
以下代码片段在我的 fork 上的custom-elements
分支上正常运行: https ://github.com/pmdartus/jsdom/tree/custom-elements
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`
<body>
<div id="container"></div>
<script>
class Component extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = "<p>Hello world</p>";
}
}
customElements.define('custom-component', Component);
const container = document.querySelector("#container");
Object.assign(container, {
innerHTML: "<custom-component></custom-component>"
})
console.log(container.innerHTML); // <custom-component></custom-component>
console.log(container.firstChild.shadowRoot.innerHTML); // <p>Hello world</p>
</script>
</body>
`, {
runScripts: "dangerously"
});
非法构造函数可能是由原始 HTMLElement 构造函数抛出的,对分支所做的更改应该为每个新窗口对象修补构造函数。 @tbranyen你有完整的复制示例,所以我可以在本地尝试吗?
嗨@pmdartus ,我不太确定是什么导致了我的问题,但我直接在你的分支中写了一些孤立的代码,效果很好:
const { JSDOM } = require('.');
const window = (new JSDOM()).window;
const { HTMLElement, customElements, document } = window;
class CustomElement extends HTMLElement {
constructor() {
super();
console.log('constructed');
}
connectedCallback() {
console.log('connected');
}
}
customElements.define('custom-element', CustomElement);
document.body.appendChild(new CustomElement());
//constructed
//connected
{
const window = (new JSDOM()).window;
const { HTMLElement, customElements, document } = window;
class CustomElement extends HTMLElement {
constructor() {
super();
console.log('Constructed');
}
connectedCallback() {
console.log('Connected');
}
}
customElements.define('custom-element', CustomElement);
document.body.appendChild(new CustomElement());
//constructed
//connected
}
这实际上是我的测试系统所做的,但会中断。 所以这可能是我的目标。
编辑:
好吧,我想我缩小了问题最有可能发生的地方。 我必须坚持创建的初始HTMLElement
构造函数。 如果我调整上面的代码以重用构造函数:
// Inside the block, second component, reuse the HTMLElement.
const { customElements, document } = window;
这将产生以下内容:
connected
/home/tbranyen/git/pmdartus/jsdom/lib/jsdom/living/helpers/create-element.js:643
throw new TypeError("Illegal constructor");
编辑2:
找到了:
// Don't reuse the previously defined Element...
global.HTMLElement = global.HTMLElement || jsdom.window.HTMLElement;
注意到这个线程已有 4 年历史,是否支持或计划支持 Web 组件?
在其中包含 Web 组件会很好,但作为替代方案,如果有人想知道......现在可以在节点中使用无头 chrome 来渲染/构建 html sting 文件。
注意到这个线程已有 4 年历史,是否支持或计划支持 Web 组件?
它是一项正在进行的工作,因为规范是逐个实施的。
polyfill 位于: https ://github.com/WebReflection/document-register-element 就像一个魅力! 向作者致以最诚挚的谢意!
对于那些在同样问题上苦苦挣扎的人,只需执行以下操作:
npm install -D document-register-element
在您的 jest 配置中设置一个设置文件,该文件将在您的所有测试之前运行:
{
"setupFilesAfterEnv": [
"./tests/setup.js"
]
}
最后,在该文件中('tests/setup.js'):
import 'document-register-element'
完成此操作后,通过 document.createElement('custom-component') 在 jsdom 中注册和创建自定义元素就完美了! 然而,片段似乎不起作用。 (顺便说一下,我没有使用 shadow dom)。
@tebanep正如你提到的那样,polyfill 不适合大多数 Web 组件工作,如果它不支持 Shadow DOM,那么它就不能与它所完成的工作进行比较。
@tebanep谢谢。 因为我不需要影子 dom,所以这是很好的信息
有希望实现这一点吗? 现在我们使用 jsdom-wc 有很多错误,但没有更好的解决方案。 我希望和祈祷这个话题。
@dknight我知道 jsdom-wc 几乎是一个让它有点工作的黑客。 我在我个人的 npm 范围内发布了具有更好兼容性的模块。 您可以使用以下方式安装它:
npm install @tbranyen/[email protected] --save-dev
我现在用它来满足我所有的 JSDOM webcomponent 需求,直到稳定下来。
@tbranyen你取消发布你的分叉了吗? 我在 npm 上找不到。
@KingHenne dangit,看起来它最终出现在我们的“企业”注册表中。 我刚刚发布到公共 npm。 对于那个很抱歉!
不要@我,但我们不应该只在真实浏览器中测试 web ui 代码,例如使用 puppeteer。 Shadow DOM/自定义元素支持问题就消失了。
如果您不想成为@'d @Georgegriff,请不要发表评论。 这是一个有效的策略,但是在其他方面它很慢而且有问题,因为你正在有效地做 IPC,是的,即使是 puppeteer。 当浏览器死机时,在很多情况下原因并不明显。 试着开玩笑地调试 puppeteer 问题,以了解为什么它并不总是最好的主意。
就我个人而言,我宁愿在同一个线程上保持同步测试。 没有理由说明规范的隔离实现不应该是测试组件的合理运行时。 JSDOM 在这一点上实际上是一个浏览器,只是不如三大浏览器稳定。
polyfill 位于: https ://github.com/WebReflection/document-register-element 就像一个魅力! 向作者致以最诚挚的谢意!
对于那些在同样问题上苦苦挣扎的人,只需执行以下操作:
npm install -D document-register-element
在您的 jest 配置中设置一个设置文件,该文件将在您的所有测试之前运行:
{ "setupFilesAfterEnv": [ "./tests/setup.js" ] }
最后,在该文件中('tests/setup.js'):
import 'document-register-element'
完成此操作后,通过 document.createElement('custom-component') 在 jsdom 中注册和创建自定义元素就完美了! 然而,片段似乎不起作用。 (顺便说一下,我没有使用 shadow dom)。
它对我来说很好,但是connectedCallback
从来没有被调用过,知道吗?
@FaBeyyy对我来说也一样 :(
@FaBeyyy @majo44您必须将组件附加到文档中,即。 document.body.appendChild(...)
为connectedCallback
被解雇。 根据规范,当组件附加到 Dom 时会调用它。
JSDOM 在这一点上实际上是一个浏览器,只是不如三大浏览器稳定。
在这一点上,它更像是两大巨头,因为微软正在抛弃他们的,与 Windows 一样久的微软。
@FaBeyyy @majo44您必须将组件附加到文档中,即。
document.body.appendChild(...)
为connectedCallback
被解雇。 根据规范,当组件附加到 Dom 时会调用它。
很明显,感谢船长,但这当然不是这里的问题。 如果我不知道组件生命周期是如何工作的,我可能不会尝试编写测试😄。 当我找到时间时,将创建一个回购展示。
@FaBeyyy
所以我找到了适合我的设置。 我不得不为 MutationObserver 添加 polyfill。 我正在使用 JSDOM 和 Jest 来测试 porpoise,工作设置是:
// package.json
{ ...
"jest": {
"transform": {
"^.+\\.(mjs|jsx|js)$": "babel-jest"
},
"setupFiles": [
"<rootDir>/node_modules/babel-polyfill/dist/polyfill.js",
"<rootDir>/node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
"<rootDir>/node_modules/document-register-element/build/document-register-element.node.js"
]
}
...
}
//.bablerc
{
"presets": [
["@babel/preset-env", { "modules": "commonjs"}]
]
}
@FaBeyyy
所以我找到了适合我的设置。 我不得不为 MutationObserver 添加 polyfill。 我正在使用 JSDOM 和 Jest 来测试 porpoise,工作设置是:// package.json { ... "jest": { "transform": { "^.+\\.(mjs|jsx|js)$": "babel-jest" }, "setupFiles": [ "<rootDir>/node_modules/babel-polyfill/dist/polyfill.js", "<rootDir>/node_modules/mutationobserver-shim/dist/mutationobserver.min.js", "<rootDir>/node_modules/document-register-element/build/document-register-element.node.js" ] } ... }
//.bablerc { "presets": [ ["@babel/preset-env", { "modules": "commonjs"}] ] }
不错,谢谢!
@majo44最新的 jsdom 不需要。 使用 Jest(使用 jsdom v11)时,您可以使用更新的环境: https ://www.npmjs.com/package/jest-environment-jsdom-fourteen
@mgibas谢谢,使用 jest-environment-jsdom-fourteen 它也可以正常工作,并且不需要突变观察者 polyfill(但版本是 0.1.0,单个提交包:))
是否有关于 JSDOM 当前支持哪些 Web 组件 API 的细分? 似乎支持影子 DOM,但不支持自定义元素(至少在发布分支/存储库中)?
npm install @tbranyen/[email protected] --save-dev
@tbranyen你在某处有你的 fork 的源代码吗? 很想看看差异🙂
我正在使用像@majo44建议的 jest-environment-jsdom-fifteen 以及 babel-polyfill 和 document-register-element (请参阅@mgibas答案)。 但是当我尝试检索我的 web 组件 shadow dom 进行测试时,我仍然遇到错误。
el.shadowRoot
为空:
const el;
beforeEach(async() => {
const tag= 'my-component'
const myEl = document.createElement(tag);
document.body.appendChild(myEl);
await customElements.whenDefined(tag);
await new Promise(resolve => requestAnimationFrame(() => resolve()));
el = document.querySelector(tag);
}
it(() => {
const fooContent = el.shadowRoot.querySelectorAll('slot[name=foo] > *');
})
任何解决方法的想法? 仅供参考,它已经用 Karma、Mocha、Chai 和 Jasmine 进行了测试。
我的组件没有什么特别的:
customElements.define(
'my-component',
class extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
...
}
})
编辑
为了更好地理解我的问题,我对 jsdom 15.1.1 做了一些调试。
不过,我不明白为什么它在这里是空的......
所以,Element.shadowRoot 是从 88e72ef 开始实现的
https://github.com/jsdom/jsdom/blob/1951a19d8d40bc196cfda62a8dafa76ddda6a0d2/lib/jsdom/living/nodes/Element-impl.js#L388 -L415
在 document.createElement 之后,this._shadowDom 在https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/living/nodes/Element-impl.js#L403 就可以了。 并且创建了 shadow dom 中的每个元素(使用正确的值调用元素构造函数)。
但是当我在 document.body.appendChild(el) 之后立即调用el.shadowDom
(https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/living/nodes/Element-impl .js#L408), this. _shadowRoot
为空!
之后也是一样
await customElements.whenDefined(tag);
await new Promise(resolve => requestAnimationFrame(() => resolve()));
或者即使我使用以下内容而不是文档。
document.body.innerHTML = `
<my-component id="fixture"></my-component>
`:
@nminhnguyen我想你可以在这里找到@tbranyan制作的 fork 的源代码https://github.com/tbranyen/jsdom/tree/initial-custom-elements-impl
我正在尝试测试使用 lit-html 和 lit-element 制作的 Web 组件,我在创建元素时注意到了这种差异。
const myElem = new MyElem();
document.body.appendChild(myElem);
await myElem.updateComplete;
console.log(myElem.shadowRoot) // exists and has the rendered markup
当我使用 document.createElement
const myElem = document.createElement('my-elem');
document.body.appendChild(myElem);
await myElem.updateComplete;
console.log(myElem.shadowRoot) // null
为了配置 jest,我只使用了一个 polyfill: setupFiles: ['document-register-element']
似乎myElem
中的渲染方法永远不会被调用。 进一步调试我发现 lit-element 中的方法initialize
永远不会被调用。
所以如果我这样做,第二个例子会起作用
const myElem = document.createElement('my-elem');
myElem.initialize();
document.body.appendChild(myElem);
await myElem.updateComplete;
console.log(myElem.shadowRoot) // exists and has the rendered markup
我创建了一个支持 Web 组件的替代 DOM。 我第一次尝试做一个 PR,但是 JSDOM 的工作方式让我很难在那里解决我的需求。 您可以自由使用它或查看代码。
DOM:
https://www.npmjs.com/package/happy-dom
开玩笑环境:
https://www.npmjs.com/package/jest-environment-happy-dom
看起来真棒。 谢谢你。
2019 年 10 月 7 日星期一凌晨 1:08,摩羯座 86通知@github.com 写道:
我创建了一个支持 Web 组件的替代 DOM。 我先来
试图做一个 PR,但是 JSDOM 的工作方式让我很难解决我的问题
那里需要。 您可以自由使用它和/或查看代码。DOM:
https://www.npmjs.com/package/happy-dom开玩笑环境:
https://www.npmjs.com/package/jest-environment-happy-dom—
您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/jsdom/jsdom/issues/1030?email_source=notifications&email_token=ACQ5ZD5QUEITPND4SXWOHW3QNILSRA5CNFSM4A4G5SF2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEAOO5ZA#issuecomment-538767076
或使线程静音
https://github.com/notifications/unsubscribe-auth/ACQ5ZDYU465DXI4KHBQH4KTQNILSRANCNFSM4A4G5SFQ
.
@摩羯座86
您的工作使我的测试环境变得简单,谢谢!
https://github.com/EasyWebApp/WebCell/tree/v2/MobX
@摩羯座86
您的工作使我的测试环境变得简单,谢谢!
https://github.com/EasyWebApp/WebCell/tree/v2/MobX
谢谢@TechQuery!
看起来真棒。 谢谢你。
…
2019 年 10 月 7 日星期一凌晨 1:08 摩羯座86 @ 。 * > 写道: 我创建了一个支持 Web 组件的替代 DOM。 我第一次尝试做一个 PR,但是 JSDOM 的工作方式让我很难在那里解决我的需求。 您可以自由使用它和/或查看代码。 DOM: https: //www.npmjs.com/package/happy-dom Jest 环境: https ://www.npmjs.com/package/jest-environment-happy-dom — 你收到这个是因为你订阅了这个线。 回复此电子邮件直接,查看它在GitHub <#1030?email_source =通知&email_token = ACQ5ZD5QUEITPND4SXWOHW3QNILSRA5CNFSM4A4G5SF2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEAOO5ZA#issuecomment-538767076>,或静音螺纹https://github.com/notifications/unsubscribe-auth/ACQ5ZDYU465DXI4KHBQH4KTQNILSRANCNFSM4A4G5SFQ 。
谢谢@motss!
是否有关于 JSDOM 当前支持哪些 Web 组件 API 的细分? 似乎支持影子 DOM,但不支持自定义元素(至少在发布分支/存储库中)?
我也会对此感兴趣:)
最有用的评论
TL;博士
在过去的两周里,我一直在评估在 jsdom 中添加对自定义元素的支持的可行性。 这是调查的结果。
您可以在此处找到符合规范的自定义元素实现: https ://github.com/jsdom/jsdom/compare/master...pmdartus:custom-elements?expand=1。 这里和那里仍然有一些粗糙的边缘,但至少大部分WPT-test pass 。 剩下的失败测试要么是已知的 JSDOM 问题,要么是在我们处理 jsdom 中的实际实现时可以解决的小问题。
现在好消息是,Shadow DOM 已经得到原生支持,同时使用自定义元素分支和突变观察者,我能够在 jsdom 中加载和渲染最新版本的Polymer 3 hello world 应用程序示例🎉。 在当前状态下,分支无法加载 Stencil 应用程序(Stencil 开发模式需要一些不受支持的功能,例如
module
,并且 prod 模式因未知原因抛出)。行动计划
这是之前需要首先进行的更改的列表,开始处理实际的自定义元素规范实现。 列表中的每个项目都是独立的,可以并行处理。
支持
[CEReactions]
IDL 扩展属性jsdom 中缺少的用于添加对自定义元素的支持的核心功能之一是
[CEReactions]
属性。 通过修补正确的原型属性,我部分能够解决这个问题。 只要自定义元素反应堆栈是全局的并且不是每个相关的相似来源浏览上下文的单元,这种方法就可以工作,因为所有接口原型都是共享的。这种方法有一些缺点,因为某些接口具有与索引属性相关联的
[CEReactions]
属性(HTMLOptionsCollection
,DOMStringMap
)。 在内部,jsdom 使用代理来跟踪这些属性的变化。 在这种情况下,接口的原型修补不起作用。 解决此问题的另一种方法是修补实现而不是接口(未实现)。我对 webidl2js 内部不够熟悉,但我们应该探索为
[CEReactions]
添加一个全局挂钩?变化:
ce-reaction.js
:支持
[HTMLConstructor]
IDL 扩展属性正如@domenic上面解释的那样,添加对
[HTMLConstructor]
的支持是这里的主要障碍之一。我可以通过修补每个浏览上下文的接口构造函数来解决这个问题。 接口构造函数将能够访问正确的窗口和文档对象,同时保持共享原型。 这种方法还避免了为每个新的浏览上下文重新评估界面原型的性能开销。
我不确定这是否是这里最好的方法,但它符合要求而不会引入额外的性能开销。
变化:
create-element.js
使 make 片段解析算法规范兼容 (#2522)
正如这里所讨论的,在
Element.innerHTML
和Element.outerHTML
中使用的 HTML 片段解析算法实现是不正确的。 解析算法需要重构,才能正确调用自定义元素反应回调。变化:
改进创建元素算法的界面查找
我很快偶然发现的问题之一是在添加对自定义元素创建的支持时引入了新的循环依赖项。 CustomElementRegistry 和创建元素算法都需要访问 Element 接口,从而创建循环依赖的噩梦。
分支中采用的方法是创建一个
InterfaceCache
,这将允许通过元素命名空间和名称以及通过接口名称查找接口。 接口模块在评估后被延迟评估和缓存。 这种方法摆脱了循环依赖,因为接口不再需要在顶层。这是解决 jsdom 中这个长期存在的问题的一种方法,这种方法的问题之一是它可能会破坏 jsdom 的 webpacked/browserified 版本(未经测试)。
变化:
create-element.js
~修复
Element.isConnected
以支持 Shadow DOM (https://github.com/jsdom/jsdom/pull/2424)~这是一个随着影子 DOM的引入而出现的问题,如果元素是影子树的一部分,isConnected 返回
false
。 需要在此处添加新的 WPT 测试,因为没有测试检查此行为。变化:
Node-impl.js
修复
HTMLTemplateElement.templateContents
节点文档 (#2426)规范中定义的模板内容与 HTMLTemplateElement 本身具有不同的节点文档。 jsdom 今天没有实现这种行为,并且 HTMLTemplateElement 和它的模板内容共享相同
文档节点。
变化:
HTMLTemplateElement-impl.js
htmltodom.js
。 这种变化也对解析器有一些下游影响。 如果上下文元素是 HTMLTemplateElement,则 HTML 片段解析算法应该从模板内容而不是元素本身中提取文档节点。将缺少的采用步骤添加到
HTMLTemplateElement
(#2426)HTMLTemplateElement
在被另一个文档采用时需要运行一些特定的步骤。 据我所知,土接口有一个特殊的采用步骤。 采用节点算法实现也需要更新以调用此采用步骤。变化:
HTMLTemplateElement-impl.js
Document-impl.js
在 parse5 序列化程序中添加对 isValue 查找的支持
序列化 HTML 片段算法,在序列化元素时查找与元素关联的值并将其反映为序列化内容中的属性。 在 parse5 树适配器中添加另一个钩子会很有趣,它将查找与元素
getIsValue(element: Element): void | string
关联的 is 值。如果元素具有关联的 is 值,则另一种方法(未实现)是添加更改当前
getAttrList
挂钩的行为以将 is 值返回到属性列表。表现
在进行任何性能优化之前,我还想检查分支中更改的性能。 与树突变基准的 master 上的当前结果相比,添加自定义元素会增加 10% 的性能开销。 然而,与 master 相比,新 JSDOM 环境的创建现在慢了 3 到 6 倍,这需要更深入的调查来确定根本原因。
更多详情:这里