From de08b0b65bceb1bcf210d7a817aa549d713db294 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 10 Dec 2021 10:14:40 -0600 Subject: [PATCH] fix #2. manage the history positiont to determine when to skip load event --- .../driver/cypress/integration/cy/nav_spec.js | 154 ++++++++++++++++++ packages/driver/src/cy/commands/navigation.ts | 28 +++- packages/driver/src/cy/listeners.ts | 18 ++ packages/driver/src/cypress/cy.ts | 49 +++++- 4 files changed, 233 insertions(+), 16 deletions(-) create mode 100644 packages/driver/cypress/integration/cy/nav_spec.js diff --git a/packages/driver/cypress/integration/cy/nav_spec.js b/packages/driver/cypress/integration/cy/nav_spec.js new file mode 100644 index 000000000000..e5b352b42c86 --- /dev/null +++ b/packages/driver/cypress/integration/cy/nav_spec.js @@ -0,0 +1,154 @@ +describe('chrome extra load event', () => { + it('cy.back & cy.forward', () => { + const navEvents = [] + + cy.on('navigation:changed', (e) => { + navEvents.push(e) + }) + + cy.visit('/fixtures/generic.html') + + cy.get('#hashchange').click() + cy.wait(2000) + + cy.go('back') + + cy.wait(2000) + + cy.go('forward') + + cy.wait(2000) + + cy.get('#dimensions').click() + + cy.wait(2000) + cy.go('back') + + cy.wait(2000) + cy.go('back') + + cy.wrap(navEvents) + .should('deep.equal', [ + 'page navigation event (load)', // load about:blank + 'page navigation event (before:load)', // before:load generic html + 'page navigation event (load)', // load generic.html + 'hashchange', // click generic.html#hashchange + 'hashchange', // back generic.html + 'hashchange', // forward generic.html#hashchange + 'page navigation event (before:load)', // before:load dimensions.html + 'page navigation event (load)', // load dimensions.html + 'page navigation event (before:load)', // before:load generic.html#hashchange + 'page navigation event (load)', // load generic.html#hashchange + 'hashchange', // forward generic.html + ]) + }) + + it('extra load events on hashchange', () => { + const navEvents = [] + + cy.on('navigation:changed', (e) => { + navEvents.push(e) + }) + + cy.visit('/fixtures/generic.html') + + cy.get('#hashchange').click() + .window() + .then((win) => { + win.history.back() + }) + + cy.wait(2000) + + cy.window().then((win) => { + win.history.forward() + }) + + cy.wait(2000) + + cy.get('#dimensions').click() + + cy.wait(2000) + + cy.window() + .then((win) => { + win.history.back() + }) + + cy.wait(2000) + cy.window() + .then((win) => { + win.history.back() + }) + + cy.wait(2000) + + cy.wrap(navEvents) + .should('deep.equal', [ + 'page navigation event (load)', // load about:blank + 'page navigation event (before:load)', // before:load generic html + 'page navigation event (load)', // load generic.html + 'hashchange', // click generic.html#hashchange + 'hashchange', // back generic.html + 'hashchange', // forward generic.html#hashchange + 'page navigation event (before:load)', // before:load dimensions.html + 'page navigation event (load)', // load dimensions.html + 'page navigation event (before:load)', // before:load generic.html#hashchange + 'page navigation event (load)', // load generic.html#hashchange + 'hashchange', // forward generic.html + ]) + }) + + it('extra load events on history.go', () => { + const navEvents = [] + + cy.on('navigation:changed', (e) => { + navEvents.push(e) + }) + + cy.visit('/fixtures/generic.html') + + cy.get('#hashchange').click() + .window() + .then((win) => { + win.history.go(-1) + }) + + cy.wait(2000) + + cy.window().then((win) => { + win.history.go(1) + }) + + cy.wait(2000) + + cy.get('#dimensions').click() + + cy.wait(2000) + cy.window() + .then((win) => { + win.history.back(-1) + }) + + cy.wait(2000) + cy.window() + .then((win) => { + win.history.back(-1) + }) + + cy.wrap(navEvents) + .should('deep.equal', [ + 'page navigation event (load)', // load about:blank + 'page navigation event (before:load)', // before:load generic html + 'page navigation event (load)', // load generic.html + 'hashchange', // click generic.html#hashchange + 'hashchange', // back generic.html + 'hashchange', // forward generic.html#hashchange + 'page navigation event (before:load)', // before:load dimensions.html + 'page navigation event (load)', // load dimensions.html + 'page navigation event (before:load)', // before:load generic.html#hashchange + 'page navigation event (load)', // load generic.html#hashchange + 'hashchange', // forward generic.html + ]) + }) +}) diff --git a/packages/driver/src/cy/commands/navigation.ts b/packages/driver/src/cy/commands/navigation.ts index 881d9759f862..68435ed3b1d4 100644 --- a/packages/driver/src/cy/commands/navigation.ts +++ b/packages/driver/src/cy/commands/navigation.ts @@ -129,9 +129,14 @@ const navigationChanged = (Cypress, cy, state, source, arg) => { } // start storing the history entries - const urls = state('urls') || [] + let urls = state('urls') || [] + let urlPosition = state('urlPosition') - const previousUrl = _.last(urls) + if (urlPosition === undefined) { + urlPosition = -1 + } + + const previousUrl = urls[urlPosition] // ensure our new url doesnt match whatever // the previous was. this prevents logging @@ -143,11 +148,20 @@ const navigationChanged = (Cypress, cy, state, source, arg) => { // else notify the world and log this event Cypress.action('cy:url:changed', url) - urls.push(url) + const historyNav = state('historyNav') || {} - state('urls', urls) + if (historyNav.event) { + urlPosition = urlPosition + historyNav.delta + state('historyNav', {}) + } else { + urls = urls.slice(0, urlPosition + 1) + urls.push(url) + urlPosition = urlPosition + 1 + } + state('urls', urls) state('url', url) + state('urlPosition', urlPosition) // don't output a command log for 'load' or 'before:load' events // return if source in command @@ -573,10 +587,8 @@ export default (Commands, Cypress, cy, state, config) => { }) }, - go (numberOrString, options = {}) { - const userOptions = options - - options = _.defaults({}, userOptions, { + go (numberOrString, userOptions = {}) { + const options = _.defaults({}, userOptions, { log: true, timeout: config('pageLoadTimeout'), }) diff --git a/packages/driver/src/cy/listeners.ts b/packages/driver/src/cy/listeners.ts index 5d5d652821fc..b9332b5694b2 100644 --- a/packages/driver/src/cy/listeners.ts +++ b/packages/driver/src/cy/listeners.ts @@ -4,6 +4,7 @@ import _ from 'lodash' import { handleInvalidEventTarget, handleInvalidAnchorTarget } from './top_attr_guards' const HISTORY_ATTRS = 'pushState replaceState'.split(' ') +const HISTORY_NAV_ATTRS = 'go back forward'.split(' ') let events = [] let listenersAdded = null @@ -77,6 +78,23 @@ export default { callbacks.onNavigation('hashchange', e) }) + for (let attr of HISTORY_NAV_ATTRS) { + const orig = contentWindow.history?.[attr] + + if (!orig) { + continue + } + + contentWindow.history[attr] = function (delta) { + callbacks.onHistoryNav({ + event: attr, + delta: attr === 'back' ? -1 : (attr === 'forward' ? 1 : delta), + }) + + orig.apply(this, [delta]) + } + } + for (let attr of HISTORY_ATTRS) { const orig = contentWindow.history?.[attr] diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 386d34afec3a..2d4fe8e21fce 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -21,6 +21,7 @@ import { create as createFocused, IFocused } from '../cy/focused' import { create as createMouse, Mouse } from '../cy/mouse' import { Keyboard } from '../cy/keyboard' import { create as createLocation, ILocation } from '../cy/location' +import { $Location } from '../cypress/location' import { create as createAssertions, IAssertions } from '../cy/assertions' import $Listeners from '../cy/listeners' import { $Chainer } from './chainer' @@ -480,14 +481,43 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer // proxy has not injected Cypress.action('window:before:load') // so Cypress.onBeforeAppWindowLoad() was never called return $autIframe.on('load', () => { - if (this.state('isStable')) { - // Chromium 97+ triggers fires iframe onload for cross-origin-initiated same-document - // navigations to make it appear to be a cross-document navigation, even when it wasn't - // to alleviate security risk where a cross-origin initiator can check whether - // or not onload fired to guess the url of a target frame. - // When the onload is fired, neither the before:unload or unload event is fired to remove - // the attached listeners or to clean up the current page state. - return + const historyNav = this.state('historyNav') + + if (historyNav && historyNav.event) { + const urls = this.state('urls') + const urlPosition = this.state('urlPosition') + const current = $Location.create(this.state('url')) + const delta = historyNav.delta || 0 + + const bothUrlsMatchAndOneHasHash = (current, remote) => { + const remoteHasHash = (remote.hash || remote.href.slice(-1) === '#') + const currHasHash = (current.hash || current.href.slice(-1) === '#') + + // the remote has a hash or the last char of href is a hash + return (remoteHasHash || currHasHash) && + // both must have the same origin + current.origin === remote.origin && + // both must have the same pathname + current.pathname === remote.pathname && + // both must have the same query params + current.search === remote.search + } + + if (delta !== 0) { // delta == 0 is a page refresh + const nextPosition = urlPosition + delta + const nextUrl = $Location.create(urls[nextPosition]) + + if (bothUrlsMatchAndOneHasHash(current, nextUrl)) { + // Skip load event. + // Chromium 97+ triggers fires iframe onload for cross-origin-initiated same-document + // navigations to make it appear to be a cross-document navigation, even when it wasn't + // to alleviate security risk where a cross-origin initiator can check whether + // or not onload fired to guess the url of a target frame. + // When the onload is fired, neither the before:unload or unload event is fired to remove + // the attached listeners or to clean up the current page state. + return + } + } } // if setting these props failed @@ -1084,6 +1114,9 @@ export class $Cy implements ITimeouts, IStability, IAssertions, IRetries, IJQuer // uncaught exception behavior (logging to console) return undefined }, + onHistoryNav ({ event, delta }) { + cy.state('historyNav', { event, delta }) + }, onSubmit (e) { return cy.Cypress.action('app:form:submitted', e) },