Is there a work-around for the Chrome error: "Require user gesture for beforeunload dialogs" during Cypress tests
Is there a work-around for the Chrome error: "Require user gesture for beforeunload dialogs" during Cypress tests
https://www.chromestatus.com/feature/5082396709879808
卸载前对话框需要用户手势
The beforeunload dialog will only be shown if the frame attempting to
display it has received a user gesture or user interaction (or if any
embedded frame has received such a gesture). (There will be no change
to the dispatch of the beforeunload event, just a change to whether
the dialog is shown.)
这是我们运行遇到的问题。在我们的单页应用程序中,业务规则规定我们会在用户单击浏览器后退按钮时提醒他们。
下面这段代码在我们的app.js中做了什么:
componentDidMount = () => {
window.addEventListener('beforeunload', event => {
event.returnValue = `Are you sure you want to leave?`;
});
}
如果用户在任何页面上导航离开,将弹出默认警告框。
但是在我们的 Cypress 测试中,我们有一个 beforeEach
可以追溯到每次测试之前应用程序流程的开始。这会触发 beforeunload
事件,因为我们要离开页面,但我们没有看到警报,而是收到 chrome 错误:
Require user gesture for beforeunload dialogs
有人 运行 以前参与过这个问题或者有解决方法的线索吗?
目前我唯一能想到的就是删除 beforeEach
,但之后我们需要对每个要测试的东西进行单独测试。而不是只有几页测试文件...
据我所知,目前无法以符合 "user gesture" 的方式与网站进行交互,因为赛普拉斯目前使用 Chrome 不考虑的编程浏览器 API作为真正的用户交互(这将在 native events 实施后成为可能)。
编辑: 重新阅读问题,我实际上不确定您在寻找什么。如果您真的想阻止重定向,即使是在测试期间,那么以下内容也无济于事。相反,如果您想要断言事件已正确注册,并且正在执行它应该执行的操作,请参阅下文。
也就是说,您不需要真正阻止 unload
事件(实际上您甚至不想要它,因为那样您就需要手动 confirm/cancel 对话框,这是不可能的 ATM,尽管赛普拉斯在某些情况下会自动执行此操作)。回调仍然被调用,你可以断言。
因此,您可以猴子修补事件处理程序,缓存 return 值,并在重定向后对其进行断言:
// cypress/support/index.js
const beforeUnloadRets = [];
// command used to assert on beforeunload event return values. Callback is
// retried until it doesn't throw, and is invoked with the value
// potentially-registered beforeunload handler return value. If handler was
// registered, but didn't return anything (i.e. doesn't prevent the event),
// the value is `null`. If no handler was registered, value is `undefined`.
Cypress.Commands.add('assertBeforeUnload', ( cb ) => {
cy.wrap(null, { log: false }).should(() => cb(beforeUnloadRets.shift()));
});
beforeEach(() => {
cy.on('window:before:load', ( win ) => {
// monkey-patch `window.addEventListener` in case the `beforeunload` handler
// is registered using this API
// -------------------------------------------------------------------------
const _addEventListener = win.addEventListener;
win.addEventListener = function (eventName, listener, ...rest) {
if ( eventName === 'beforeunload' ) {
const _origListener = listener;
listener = (...args) => {
const ret = _origListener(...args);
beforeUnloadRets.push(ret === undefined ? null : ret);
return ret;
}
}
return _addEventListener.call(this, eventName, listener, ...rest);
};
// monkey-patch `window.onbeforeload` in case it's registered in that way
// -------------------------------------------------------------------------
let _onbeforeunloadHandler;
win.onbeforeunload = ( ev ) => {
if ( _onbeforeunloadHandler ) {
const ret = _onbeforeunloadHandler.call(win, ev);
beforeUnloadRets.push(ret === undefined ? null : ret);
return ret;
}
};
Object.defineProperty(win, 'onbeforeunload', {
set ( handler ) {
_onbeforeunloadHandler = handler;
}
})
});
});
用法(注意,出于演示目的,我在测试中注册了 beforeunload
事件,但在实际场景中,这就是您的应用程序将执行的操作):
describe('test', () => {
it('test', () => {
// page one. Register 1 beforeunload event, and prevent the unload event.
// -------------------------------------------------------------------------
cy.visit('/a');
cy.window().then( window => {
window.addEventListener('beforeunload', () => {
return 'one';
});
});
// redirect to page two. Assert a prevented unload event.
// Register another, but don't prevent unload.
// -------------------------------------------------------------------------
cy.visit('/b');
cy.assertBeforeUnload( ret => {
expect(ret).to.eq('one');
});
cy.window().then( window => {
// register, but don't prevent
window.onbeforeunload = () => {};
});
// page three. Assert a non-prevented unload event. Register none.
// -------------------------------------------------------------------------
cy.visit('/c');
cy.assertBeforeUnload( ret => {
// assert an event fired, but returned nothing (indicated by `null`)
expect(ret).to.eq(null);
});
// page four. Assert no beforeunload event was fired.
// -------------------------------------------------------------------------
cy.visit('/d');
cy.assertBeforeUnload( ret => {
expect(ret).to.eq(undefined);
});
});
});
我们无法禁用 Chrome 操作来检查用户是否进行过交互,因此我们想出了一个简单的解决方法:
/* istanbul ignore next */
componentDidMount = () => {
if (process.env.NODE_ENV === 'production') {
window.addEventListener('beforeunload', onBrowserBack);
}
}
/* istanbul ignore next */
componentDidUpdate() {
// If another error modal is up, DO NOT trigger the beforeunload alert
if (process.env.NODE_ENV === 'production' && this.props.hasError) {
window.removeEventListener('beforeunload', onBrowserBack);
}
}
基本上,beforeunload 警报模式现在只会显示在 production
环境中,而当我们进行 cypress 测试时,它们不会显示。
https://www.chromestatus.com/feature/5082396709879808
卸载前对话框需要用户手势
The beforeunload dialog will only be shown if the frame attempting to display it has received a user gesture or user interaction (or if any embedded frame has received such a gesture). (There will be no change to the dispatch of the beforeunload event, just a change to whether the dialog is shown.)
这是我们运行遇到的问题。在我们的单页应用程序中,业务规则规定我们会在用户单击浏览器后退按钮时提醒他们。
下面这段代码在我们的app.js中做了什么:
componentDidMount = () => {
window.addEventListener('beforeunload', event => {
event.returnValue = `Are you sure you want to leave?`;
});
}
如果用户在任何页面上导航离开,将弹出默认警告框。
但是在我们的 Cypress 测试中,我们有一个 beforeEach
可以追溯到每次测试之前应用程序流程的开始。这会触发 beforeunload
事件,因为我们要离开页面,但我们没有看到警报,而是收到 chrome 错误:
Require user gesture for beforeunload dialogs
有人 运行 以前参与过这个问题或者有解决方法的线索吗?
目前我唯一能想到的就是删除 beforeEach
,但之后我们需要对每个要测试的东西进行单独测试。而不是只有几页测试文件...
据我所知,目前无法以符合 "user gesture" 的方式与网站进行交互,因为赛普拉斯目前使用 Chrome 不考虑的编程浏览器 API作为真正的用户交互(这将在 native events 实施后成为可能)。
编辑: 重新阅读问题,我实际上不确定您在寻找什么。如果您真的想阻止重定向,即使是在测试期间,那么以下内容也无济于事。相反,如果您想要断言事件已正确注册,并且正在执行它应该执行的操作,请参阅下文。
也就是说,您不需要真正阻止 unload
事件(实际上您甚至不想要它,因为那样您就需要手动 confirm/cancel 对话框,这是不可能的 ATM,尽管赛普拉斯在某些情况下会自动执行此操作)。回调仍然被调用,你可以断言。
因此,您可以猴子修补事件处理程序,缓存 return 值,并在重定向后对其进行断言:
// cypress/support/index.js
const beforeUnloadRets = [];
// command used to assert on beforeunload event return values. Callback is
// retried until it doesn't throw, and is invoked with the value
// potentially-registered beforeunload handler return value. If handler was
// registered, but didn't return anything (i.e. doesn't prevent the event),
// the value is `null`. If no handler was registered, value is `undefined`.
Cypress.Commands.add('assertBeforeUnload', ( cb ) => {
cy.wrap(null, { log: false }).should(() => cb(beforeUnloadRets.shift()));
});
beforeEach(() => {
cy.on('window:before:load', ( win ) => {
// monkey-patch `window.addEventListener` in case the `beforeunload` handler
// is registered using this API
// -------------------------------------------------------------------------
const _addEventListener = win.addEventListener;
win.addEventListener = function (eventName, listener, ...rest) {
if ( eventName === 'beforeunload' ) {
const _origListener = listener;
listener = (...args) => {
const ret = _origListener(...args);
beforeUnloadRets.push(ret === undefined ? null : ret);
return ret;
}
}
return _addEventListener.call(this, eventName, listener, ...rest);
};
// monkey-patch `window.onbeforeload` in case it's registered in that way
// -------------------------------------------------------------------------
let _onbeforeunloadHandler;
win.onbeforeunload = ( ev ) => {
if ( _onbeforeunloadHandler ) {
const ret = _onbeforeunloadHandler.call(win, ev);
beforeUnloadRets.push(ret === undefined ? null : ret);
return ret;
}
};
Object.defineProperty(win, 'onbeforeunload', {
set ( handler ) {
_onbeforeunloadHandler = handler;
}
})
});
});
用法(注意,出于演示目的,我在测试中注册了 beforeunload
事件,但在实际场景中,这就是您的应用程序将执行的操作):
describe('test', () => {
it('test', () => {
// page one. Register 1 beforeunload event, and prevent the unload event.
// -------------------------------------------------------------------------
cy.visit('/a');
cy.window().then( window => {
window.addEventListener('beforeunload', () => {
return 'one';
});
});
// redirect to page two. Assert a prevented unload event.
// Register another, but don't prevent unload.
// -------------------------------------------------------------------------
cy.visit('/b');
cy.assertBeforeUnload( ret => {
expect(ret).to.eq('one');
});
cy.window().then( window => {
// register, but don't prevent
window.onbeforeunload = () => {};
});
// page three. Assert a non-prevented unload event. Register none.
// -------------------------------------------------------------------------
cy.visit('/c');
cy.assertBeforeUnload( ret => {
// assert an event fired, but returned nothing (indicated by `null`)
expect(ret).to.eq(null);
});
// page four. Assert no beforeunload event was fired.
// -------------------------------------------------------------------------
cy.visit('/d');
cy.assertBeforeUnload( ret => {
expect(ret).to.eq(undefined);
});
});
});
我们无法禁用 Chrome 操作来检查用户是否进行过交互,因此我们想出了一个简单的解决方法:
/* istanbul ignore next */
componentDidMount = () => {
if (process.env.NODE_ENV === 'production') {
window.addEventListener('beforeunload', onBrowserBack);
}
}
/* istanbul ignore next */
componentDidUpdate() {
// If another error modal is up, DO NOT trigger the beforeunload alert
if (process.env.NODE_ENV === 'production' && this.props.hasError) {
window.removeEventListener('beforeunload', onBrowserBack);
}
}
基本上,beforeunload 警报模式现在只会显示在 production
环境中,而当我们进行 cypress 测试时,它们不会显示。