您是要请求一个 _feature_ 还是报告一个 _bug_? 报告错误
目前的行为是什么?
从测试套件内部调用以下内容:
Object.defineProperty(location, "hostname", {
value: "example.com",
writable: true
});
引发以下错误:
TypeError: Cannot redefine property: hostname
at Function.defineProperty (<anonymous>)
什么是预期行为?
代码不应抛出异常, window.location.hostname === "example.com"
应评估为真。
从它的外观来看, jsdom
现在将 window.location 设置为不可伪造。 更改window.location
值的唯一方法是使用reconfigure
,但是(根据 #2460)Jest 不会公开jsdom
供测试使用。
请提供您的确切 Jest 配置并提及您的 Jest、节点、yarn/npm 版本和操作系统。
玩笑版本:22.0.1
节点版本:8.6.0
纱线版本:1.2.0
操作系统:macOS High Sierra 10.13.2
我有类似的问题。 您可以像这样创建自己的 JSDOMEnvironment 并将 jsdom 对象公开给全局对象。
const JSDOMEnvironment = require('jest-environment-jsdom');
module.exports = class CustomizedJSDomEnvironment extends JSDOMEnvironment {
constructor(config) {
super(config);
this.global.jsdom = this.dom;
}
teardown() {
this.global.jsdom = null;
return super.teardown();
}
};
然后你可以在你的测试用例中随意调用 jsdom.reconfigure
这是一个很好的解决方法,谢谢分享!
你应该返回super.teardown();
因为它是一个承诺,顺便说一句
完美, @oliverzy - 我会尝试一下。 谢谢!
有没有合适的地方来记录这个? 这似乎是合理的经常出现的一个问题; 希望如果将其集成到文档中,可以减少未来的问题?
这个解决方案不太奏效。
在我们的测试文件中,似乎global
被设置为 JSDom 的window
对象。
换句话说,在测试套件中, global
与window
,但在扩展JSDOMEnvironment
, global
来自 Node 的环境。
结果,有了这个:
describe("test suite", () => {
it("should not fail", () => {
global.jsdom.reconfigure({
url: "https://www.example.com/"
});
});
});
失败,因为global.jsdom
未定义。
我通过这样做绕过了它,但我并不大惊小怪。
const JSDOMEnvironment = require("jest-environment-jsdom");
module.exports = class JSDOMEnvironmentGlobal extends JSDOMEnvironment {
constructor(config) {
super(config);
this.dom.window.jsdom = this.dom;
}
};
在这种环境下,测试套件中的global.jsdom
等于this.dom
,并且上面的测试套件有效。
对我来说,感觉就像将jsdom
设置为它自己的window
对象的属性最终肯定会崩溃 - 有没有更干净的方法来做到这一点?
您需要在测试中编写jsdom
而不是global.jsdom
。
@oliverzy喜欢这个吗?
describe("test suite", () => {
it("should not fail", () => {
jsdom.reconfigure({
url: "https://www.example.com/"
});
});
});
这会抛出jsdom is not defined
,但我可能会误解。
@ simon360请配置testEnvironment
与@oliverzy代码,看看https://facebook.github.io/jest/docs/en/configuration.html#testenvironment -string
@danielbayerlein我的 Jest 配置有这个:
"testEnvironment": "@wel-ui/jest-environment-jsdom-global"
其中@wel-ui/jest-environment-jsdom-global
是我们的 monorepo 中的包的名称。 环境是越来越正确使用,不过,因为这套解决方案jsdom
的window
按预期工作。
顺便说一句,有谁知道为什么原始解决方案在新版本中不起作用?
这个:
Object.defineProperty(location, "hostname", {
value: "example.com",
writable: true
});
@SimenB 明白了。 刚刚发现一个描述jsdom的reconfigure
方法。
window 上的 top 属性在规范中被标记为 [Unforgeable],这意味着它是一个不可配置的自己的属性,因此不能被 jsdom 内运行的正常代码覆盖或隐藏,即使使用 Object.defineProperty。
我添加了一个新的存储库来演示这种行为。 有没有人能够通过在本地克隆来复制它?
@simon360转载
@simon360我找到了。 您在定义global.jsdom
时错过了this
关键字:
const JSDOMEnvironment = require("jest-environment-jsdom");
module.exports = class JSDOMEnvironmentGlobal extends JSDOMEnvironment {
constructor(config) {
super(config);
this.global.jsdom = this.dom;
}
teardown() {
this.global.jsdom = null;
return super.teardown();
}
};
location.search
呢? 我没有发现任何关于它的提及https://github.com/tmpvar/jsdom/blob/05a6deb6b91b4e02c53ce240116146e59f7e14d7/README.md#reconfiguring -the-jsdom-with-reconfiguresettings
@andrewBalekha这个怎么样?
jsdom.reconfigure({
url: 'https://www.example.com/endpoint?queryparam1=15&queryparam2=test'
});
谢谢@modestfake - 抱歉犯了愚蠢的错误!
好的,我现在看到了 - Jest 环境对象上的this.global
在 Jest 测试文件中设置为global
。 这是有道理的 - 感谢您帮助我度过难关! 如果有足够的兴趣,我可以打包该 repo 的修复版本并将其放在npm
作为jest-environment-jsdom-global
。
但是,我确实希望将来在 Jest 中有一种更简洁的方法来做到这一点。 这不是改变window.location
的低摩擦方式 -
会不会有一个新的文档块,就像@jest-environment
? 例如...
/**
* @jest-url https://www.example.com/
*/
或者,也许 JSDom 可以暴露在jest
对象的特殊部分——比如:
jest.environment.jsdom.reconfigure({
url: "https://www.example.com/"
});
(这将具有能够更改window.top
的额外好处)
我们现在已经合并了#5003。 能够将其添加为文档块可能有意义,但不确定。 @cpojer? 我们也可以弃用testUrl
,因为它可以通过该新选项提供。
如果有足够的兴趣,我可以打包该 repo 的修复版本并将其作为
jest-environment-jsdom-global
放在npm
jest-environment-jsdom-global
。
我认为这在任何情况下都是有意义的,因为它不仅仅是让您设置 url - 它向环境公开完整的 JSDOM
@andrewBalekha Object.defineProperty(location, 'search', { ...options });
抛出与window.location
相同的错误。 (还是)感谢你的建议。
Object.defineProperty(window.location, 'href', {
设置:newValue => { currentUrl = newValue; },
});
我在以前的版本中有这个,现在抛出错误。
如果我添加 writable: true
引发另一个异常,我无法同时指定访问器和可写
我在 npm 上发布了一个名为jest-environment-jsdom-global
的新包,这可能有助于解决某些人在使用Object.defineProperty
遇到的问题。
有没有人有{ writable: true }
的解决方法?
例如:
Object.defineProperty(window.location, 'href', { writable: true })
...
Object.defineProperty(window.location, 'hash', { writable: true })
...
Object.defineProperty(window.location, 'search', { writable: true })
@danielbayerlein阅读此线程。 您需要创建自定义环境。 上一条消息包含带有示例的 url
@modestfake我已经阅读了这个线程,并且https://github.com/facebook/jest/issues/5124#issuecomment -352749005 工作正常。 但我还有另一个用例。 使用 Jest 21.xx 我设置Object.defineProperty(window.location, 'href', { writable: true })
没有 URL 的{ writable: true }
。 如果我设置了 URL,那么测试就没有意义了。
@danielbayerlein什么用例使它可写但实际上不覆盖它? 也许理解这一点可以帮助我想出解决方法
我有一个更改 URL 的函数。
路由.js
...
export function redirectToErrorPage () {
window.location.href = '/error.html'
}
...
路由测试.js
test('redirect to the error page', () => {
...
expect(window.location.href).toBe('/error.html')
...
})
使用 Jest 21.xx 我设置了Object.defineProperty(window.location, 'href', { writable: true })
我建议切换到window.location.assign
,这样你就可以模拟这个函数。
@simon360 很有魅力! 谢谢你。 🤝
我用了
history.pushState({}, "page 2", "/bar.html");
以及开玩笑配置中的testURL
位置部分不是唯一的问题。 我不能单独导入jsdom
来调用jsdom.reconfigureWindow
(因为该函数在最新版本的 jsdom 中不再存在)。 我这样做是为了测试window !== top
运行方式不同的代码。 在最新版本的 jest 中不再有办法实现这一点,它使用了较新版本的 jsdom。
@andyearnshaw jsdom.reconfigure
方法在其对象中有一个windowTop
成员。
如果您使用的是我上面链接的 JSDOM 环境( jest-environment-jsdom-global
),您可以这样做:
jsdom.reconfigure({
windowTop: YOUR_VALUE
});
在您的测试中模拟出最高值。
我正在使用它,效果很好,谢谢!
2018 年 1 月 30 日,星期二,13:40 simon360, notifications @github.com 写道:
@andyearnshaw https://github.com/andyearnshaw jsdom.reconfigure
方法在其对象中有一个 windowTop 成员。如果您使用的是 JSDOM 环境 (jest-environment-jsdom-global) 我
上面链接,你可以这样做:jsdom.reconfigure({
窗口顶部:YOUR_VALUE
});在您的测试中模拟出最高值。
—
你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/facebook/jest/issues/5124#issuecomment-361595999或静音
线程
https://github.com/notifications/unsubscribe-auth/ABvdEzCLlWtzr0udscL0C6KUxpgXHZRhks5tPxvJgaJpZM4RGN7C
.
使用window.history.pushState
和testUrl
对我有用
https://github.com/facebook/jest/issues/5124#issuecomment -359411593
由于 Jest 方面无事可做,因此要关闭它,并且存在解决方法(请继续讨论!)
从 jsdom v8.5.0 升级到 v11.6.2 为我解决了这个问题。 所以我的package.json
包括:
"jest": "^21.2.1",
"jest-cli": "^21.2.1",
"jsdom": "^11.6.2",
如果我将 jest 和 jest-cli 升级到 v22.2.2,它会中断。
@andr-3-w 在 package.json 中有 jsdom 的任何特殊原因? 它与 Jest 捆绑在一起。
@SimenB ,好问题,我们的package.json
不需要jsdom
package.json
。 谢谢!
因此,在不直接使用 jsdom 的情况下,这是我为我的东西提出的解决方案:
适用于 Jest 21.2.1(我对此进行了测试):
进入你的 Jest 设置(例如我将使用 package.json):
"jest": {
"testURL": "http://localhost"
}
现在您将能够更改 window.location 对象,然后您可以在测试期间将 URL 设置为您喜欢的任何内容。
it('Should set href url to testURL', () => {
// Here I set href to my needs, opinionated stuff bellow
const newUrl = 'http://localhost/editor.html/content/raiweb/it/news/2018/02/altered-carbon-best-cyberpunk-tv-series-ever.html';
Object.defineProperty(window.location, 'href', {
writable: true,
value: newUrl
});
console.log(window.location.href);
});
it('Should set pathname url to testURL', () => {
// Here I set href to my needs, opinionated stuff bellow
const newUrl = '/editor.html/content/raiweb/it/news/2018/02/altered-carbon-best-cyberpunk-tv-series-ever.html';
Object.defineProperty(window.location, 'pathname', {
writable: true,
value: newUrl
});
console.log(window.location.pathname);
});
希望这对某人有所帮助。
停止发布此内容,它不适用于开玩笑”:“^ 22.4.2”
@UserNT我注意到它在这里工作的版本,我广泛地在生产测试服上使用它。 如果它不适用于较新的版本,抱歉,请提出您自己的解决方案,而不仅仅是随机抨击。
只是为了完整性,因为解决方案搁浅在这个线程的中间......
@petar-prog91 的解决方案适用于 Jest 21,但不适用于带有更新的jsdom
Jest 22。
为了在最新的 Jest 上运行,使用类似jest-environment-jsdom-global
(完全公开,这是我的包)来公开jsdom
对象并使用jsdom.reconfigure
,它将具有相同(或至少类似)的效果。
https://github.com/facebook/jest/issues/5124#issuecomment -359411593 也适用于我的 jest 22
@simon360嗨,如果我需要破解location.href
的 setter 该怎么办?
我以前的测试有很多这样的测试,现在都失败了......
const setHrefMockFn = jest.fn();
beforeAll(() => {
Object.defineProperty(location, "href", {
get: () => "https://xxxxx",
set: setHrefMockFn
});
});
it("xxx", () => {
//...
expect(setHrefMockFn.mock.calls[0][0]).toBe(xxx);
});
@simon360你能给我一个例子,如何用你的库更新下面的测试
beforeAll(() => {
=======
Object.defineProperty(window.location, 'href', {
writable: true
});
});
@abhijeetNmishra你看过文档吗? 我相信这会回答你的问题。
@simon360是的,根据我对文档的理解,大约需要
jsdom.reconfigure({
url: "https://www.example.com/"
});
全局覆盖 url 而不是每个测试。 请帮忙!
@abhijeetNmishra我不确定这个问题是讨论的最佳场所。 你介意在jest-environment-jsdom-global
存储库上打开一个问题,我们可以解决它吗? 谢谢!
@SimenB所述的解决方法(“使用jest-environment-jsdom-global
”)感觉像是一个非常次优的解决方案,显然是一个非常普遍的问题。 升级到 Jest 22 的任何人现在都需要向该第三方包添加依赖项,并且(从用户的角度)重写他们的一些测试。 这是 Jest 行为的倒退。
是否有解决方案可以构建到默认的jest-environment-jsdom
? 很高兴在您的指导下进行 PR。
非常不幸的是,有人为了改变 window.location.href 而不得不跳过这么多圈。 我刚刚开始使用 Jest,鉴于这个问题,我将重新考虑我对测试框架的选择。 真的没有比上面建议的丑陋黑客更好的解决方案了吗?
@ydogandjiev随时为解决此问题的项目做出贡献。 请记住,这是开源的,因此大肆评论诸如“不可接受”和“荒谬”之类的评论对任何人都没有帮助。
@msholty-fd 如果可以,我很乐意提供帮助。 由于我刚刚开始使用 jest 和 jsdom,我不确定我对这些库是否有足够深入的了解,无法确切知道改善这种体验的最佳途径是什么。 这是在 jest 还是 jsdom 中最好的解决方法? 这些库中的一个似乎在某个时候做出了改变,破坏了 Object.defineProperty 方法; 那是在 jest 还是 jsdom 中进行的更改?
因此,根据更改 window.location 所需的行数,以下是我认为更可取的所有选项; 这些目前都不起作用:
window.location.href = "https://www.example.com";
使用 Object.defineProperty 不起作用,因为 JSDOM 已经使窗口的位置属性[Unforgeable] :
Object.defineProperty(window.location, "href", {
value: "https://www.example.com",
configurable: true
});
创建 jsdom 的实例并配置它不起作用,因为 jest 似乎使用了自己的实例,该实例未公开以便于访问:
import { JSDOM } from "jsdom";
...
const dom = new JSDOM();
dom.reconfigure({ url: "https://www.example.com" });
使选项 1 或 2 工作需要 jsdom 回溯他们当前的目标,即表现得更像一个真正的浏览器。 因此,似乎我们唯一的选择是更容易地重新配置 jest 使用的 jsdom 实例。 直接在全局 jest 对象上公开该实例是否有意义? 即允许这样的事情:
jest.dom.reconfigure({ url: "https://www.example.com" });
我仍然认为location.assign('some-url')
比location.href = 'some-url'
更好。 我发现一个函数调用比赋值更明确,你可以模拟这个函数
@SimenB在代码试图location.href
,是的, location.assign()
更好。 但是,如果您正在测试 _reads_ location.href
, location.assign()
并不能解决问题,尤其是因为 JSDOM 中的location.assign()
实际上并没有做任何事情。
使用reconfigure
背后的想法是激活仅在location.href
以特定方式形成时才启用的代码路径。 在我们的例子中,我们有一些根据当前域更改的代码 - 代码很臭,是的,但这也是必要的,减轻臭味代码的最佳方法是使用测试装置来捕获行为并确保它保持不变到位。
有没有办法将它与 Enzyme 和已安装的组件一起使用来测试重定向?
在升级 Jest 之前通过了以下测试:
``
it('路由到正确的路由', () => {
Object.defineProperty(window.location, 'href', {
writable: true,
value: 'https://mysuperawesomesite.com/',
});
const component = mount(
<App {...props} />
);
const link = component.find('.class');
link.simulate('click');
expect(window.location.href).toEqual('https://mysuperawesomesite.com/new');
});
After upgrading Jest and implementing [jest-environment-jsdom-global](https://www.npmjs.com/package/jest-environment-jsdom-global), I tried the following to no avail:
```
it('routes to correct route', () => {
jsdom.reconfigure({
url: 'https://mysuperawesomesite.com/',
});
const component = mount(
<App {...props} />
);
const link = component.find('.class');
link.simulate('click');
expect(window.location.href).toEqual('https://mysuperawesomesite.com/new');
});
(window.location.href 仍然等于' https://mysuperawesomesite.com/ ',没有被更改为('https://mysuperawesomesite.com/new')。
使用该方法时元素上的click事件不会重定向,通过设置window.location.href来实现重定向。
不清楚如何正确测试这一点,或者之前使用 Object.defineProperty 的测试开始时构建得不好。 在此先感谢您的帮助。
编辑:已解决
能够通过使用 window.location.assign(url) 而不是 window.location.href = href 来解决这个问题。 这让我可以删除assign方法并测试它是否被正确调用。 见下文:
it('routes to correct route', () => {
window.location.assign = jest.fn();
const component = mount(
<App {...props} />
);
const link = component.find('.class');
link.simulate('click');
expect(window.location.assign).toBeCalledWith('https://mysuperawesomesite.com/new');
window.location.assign.mockRestore();
});
@SimenB .assign
和.href
之间有很大区别,您可以在 MDN 上阅读。 第一个有一个主要的跨域限制。 在我的代码中,我想从运行我的代码的 iframe 重定向父页面。 这些是跨域的。 我能做到这一点的唯一方法是更改href
。
如果重新打开这个问题,我会很高兴,因为我没有看到当前的解决方法,我将不得不不测试此功能。 这显然很糟糕。
我和@soswow 在同一条船上。 如果您能提供一种覆盖 url 的机制,那就太好了,因为我还删除了几个单元测试,直到此功能恢复为止。
如果重新打开这个问题,我会很高兴,因为我没有看到当前的解决方法,我将不得不不测试此功能。 这显然很糟糕。
在开玩笑的方面我们无能为力。 我相信 jsdom 会喜欢支持它的 PR。 https://github.com/jsdom/jsdom/issues/2112
这是一个有效的简单解决方案。
describe('changing location', () => {
const testURL = location.href;
beforeEach(() => history.replaceState({}, 'Login', '/login'));
afterEach(() => history.replaceState({}, 'Home', '/'));
it('works', () => {
expect(location.pathname).toBe('/login');
});
});
@vastus正如我明确指出的那样 - 问题在于跨域。 据我所知,history API 不允许切换到不同的域。
由于location
不能直接在 jsdom window
对象上覆盖,一种可能的方法是在派生对象上覆盖它:
global.window = Object.create(window);
Object.defineProperty(window, 'location', {
value: {
href: 'http://example.org/'
}
});
我用它来解决这个问题:
const windowLocation = JSON.stringify(window.location);
delete window.location;
Object.defineProperty(window, 'location', {
value: JSON.parse(windowLocation)
});
灵感来自@RubenVerborgh & annemarie35
添加一个基于@vastus的解决方案测试location.search
的示例:
test('gets passed query param and returns it as a string if it exists', () => {
history.replaceState({}, 'Test', '/test?customer=123');
const customerId = getQueryParam('customer');
expect(customerId).toBe('123');
});
@RubenVerborgh 很有魅力。
尝试:
window.history.pushState({}, null, '/pathname?k=v');
类似于@sahalsaad的解决方案:
```javascript
const oldWindow = window.location;
删除 window.location;
window.location = {
...旧窗口,
// 包括任何自定义覆盖,例如以下 sinon 存根
替换: sinon.stub(),
};
// 发挥你的魔法
window.location = oldWindow;
````
@sahalsaad谢谢! 我使用了您的解决方案的变体来模拟 window.location.search:
const location = {
...window.location,
search: queryString,
};
Object.defineProperty(window, 'location', {
writable: true,
value: location,
});
可能是更好的解决方案:
import { URL } from 'whatwg-url';
const location = new URL(window.location.href);
location.assign = jest.fn()
location.replace = jest.fn()
location.reload = jest.fn()
delete window.location
window.location = location
使用@kdelmonte给出的解决方案解决了我的问题,我不得不模拟window.location.search
变量。 所以我用过
window.history.pushState({}, null, '?skuId=1234')
由于
location
不能直接在 jsdomwindow
对象上覆盖,一种可能的方法是在派生对象上覆盖它:global.window = Object.create(window); Object.defineProperty(window, 'location', { value: { href: 'http://example.org/' } });
您的答案是唯一适合我的情况的答案,谢谢!
由于
location
不能直接在 jsdomwindow
对象上覆盖,一种可能的方法是在派生对象上覆盖它:global.window = Object.create(window); Object.defineProperty(window, 'location', { value: { href: 'http://example.org/' } });
您的答案是唯一适合我的情况的答案,谢谢!
这不再有效😢
由于
location
不能直接在 jsdomwindow
对象上覆盖,一种可能的方法是在派生对象上覆盖它:global.window = Object.create(window); Object.defineProperty(window, 'location', { value: { href: 'http://example.org/' } });
您的答案是唯一适合我的情况的答案,谢谢!
这不再有效😢
也不适合我
我解决了这个问题:
delete window.location
window.location = {
href: 'http://example.org/,
}
我正在使用以下模拟作为使用Location
的实用程序
export class MockLocation extends URL implements Location {
ancestorOrigins: any = []
toString = jest.fn().mockImplementation(() => this.toString())
assign = jest.fn(href => this.href = href)
replace = jest.fn(href => this.href = href)
reload = jest.fn()
constructor(
url: string = 'http://mock.localhost',
) {
super(url)
}
onWindow(window: Window) {
Object.defineProperty(window, 'location', {
writable: true,
value: this
});
return this
}
}
然后在我的测试中
let location: MockLocation
beforeEach(() => {
location = new MockLocation(MOCK_PARTNER_URL).onWindow(window)
})
我发现自己一直在处理这些棘手的对象,并创建了一个灵活的辅助函数:
export const safelyStubAndThenCleanup = (target, method, value) => {
const original = target[method]
beforeEach(() => {
Object.defineProperty(target, method, { configurable: true, value })
})
afterEach(() => {
Object.defineProperty(target, method, { configurable: true, value: original })
})
}
然后用法:
describe('when on /pages', () => {
safelyStubAndThenCleanup(window, 'location', { pathname: '/pages' })
it('should do something neat', () => { /* ... */ })
})
你可以存根你想要的任何东西: pathname
, href
等等......这让你获得了额外的免费清理好处。
关键是你不能弄乱location
本身,所以只需将location
换成假的,然后在测试完成后将其放回原处。
正如我在调试会话中看到的, global.location
是通过 getter 实现的,而不是一个简单的属性。 像这样重新定义它不是更安全吗?
let originalLocationDescriptor;
beforeAll(() => {
originalLocationDescriptor = Object.getOwnPropertyDescriptor(global, 'location');
delete global.location;
global.location = {};
});
afterAll(() => {
Object.defineProperty(global, 'location', originalLocationDescriptor);
}):
虽然很难想象我为什么要使用原始的global.location
,但它似乎更正确一些。
当然,这段代码对我来说很好用。 我只访问location.pathname
,但如果需要,可以使用jest.fn()
轻松扩展此对象。
这对我有用,使用 jest 26.5
function stubLocation(location) {
beforeEach(() => {
jest.spyOn(window, "location", "get").mockReturnValue({
...window.location,
...location,
});
});
}
stubLocation({ pathname: "/facebook/jest/issues/5124" });
test("mocks location prop", () => {
expect(window.location.pathname).toEqual("/facebook/jest/issues/5124");
});
添加一个基于@vastus的解决方案测试
location.search
的示例:test('gets passed query param and returns it as a string if it exists', () => { history.replaceState({}, 'Test', '/test?customer=123'); const customerId = getQueryParam('customer'); expect(customerId).toBe('123'); });
这对于我遇到的问题非常有效
最有用的评论
我在 npm 上发布了一个名为
jest-environment-jsdom-global
的新包,这可能有助于解决某些人在使用Object.defineProperty
遇到的问题。