Skip to content

Commit

Permalink
fix #2. manage the history positiont to determine when to skip load e…
Browse files Browse the repository at this point in the history
…vent
  • Loading branch information
emilyrohrbough committed Dec 10, 2021
1 parent 6096cd1 commit de08b0b
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 16 deletions.
154 changes: 154 additions & 0 deletions 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
])
})
})
28 changes: 20 additions & 8 deletions packages/driver/src/cy/commands/navigation.ts
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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'),
})
Expand Down
18 changes: 18 additions & 0 deletions packages/driver/src/cy/listeners.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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]

Expand Down
49 changes: 41 additions & 8 deletions packages/driver/src/cypress/cy.ts
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
},
Expand Down

0 comments on commit de08b0b

Please sign in to comment.