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 测试时,它们不会显示。