Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: AUT Redirect bug on form / anchor target #16394

Merged
merged 34 commits into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
72a9afb
Add failing test for #3975
tgriesser Apr 14, 2021
d2d8f21
Proposed fix for #3975
tgriesser Apr 14, 2021
5be3049
Pass a function wrapper rather than new Function
tgriesser Apr 15, 2021
379eaba
Merge branch 'develop' into tgriesser/fix/3975-redirect-bug
tgriesser Apr 15, 2021
fbf8e54
Add eventHandlerWrap to XHR
tgriesser Apr 15, 2021
97073d0
Merge branch 'tgriesser/fix/3975-redirect-bug' of github.com:cypress-…
tgriesser Apr 15, 2021
5d954b1
Change impl to set bridge on window, fallback to Fn ctor
tgriesser Apr 15, 2021
86c3212
Fix object lookup syntax
tgriesser Apr 15, 2021
135b55b
Fix test
tgriesser Apr 16, 2021
94f1fa4
Pairing with @brian-mann
tgriesser Apr 16, 2021
7609f85
Fix tests
tgriesser Apr 26, 2021
88b2589
Merge branch 'develop' into tgriesser/fix/3975-redirect-bug
tgriesser Apr 26, 2021
f2c0ea0
Ensure we have contentWindowListener
tgriesser Apr 26, 2021
341e745
Consolidate & document impl
tgriesser Apr 26, 2021
dfb5b57
Merge branch 'develop' into tgriesser/fix/3975-redirect-bug
tgriesser Apr 26, 2021
9fdc0e2
Split out tests for #1244
tgriesser Apr 28, 2021
267c8ba
Cleanup implementation, wrapping only needed on direct patches
tgriesser Apr 28, 2021
b8e0cee
Give a more descriptive bridge fn name for the debugger
tgriesser Apr 28, 2021
2d06c65
Forgot to save file
tgriesser Apr 28, 2021
8fd4a6c
Fixes
tgriesser Apr 28, 2021
447c0ad
Guard against event already having target descriptor
tgriesser Apr 28, 2021
bc722aa
Fix window stub
tgriesser Apr 29, 2021
745667c
Merge branch 'develop' into tgriesser/fix/3975-redirect-bug
tgriesser Apr 29, 2021
dc956eb
Merge branch 'develop' into tgriesser/fix/3975-redirect-bug
tgriesser Apr 29, 2021
3d0a5e6
Merge branch 'develop' into tgriesser/fix/3975-redirect-bug
tgriesser May 3, 2021
23d86f7
Merge branch 'develop' into tgriesser/fix/3975-redirect-bug
tgriesser May 4, 2021
42358a9
improve tests for 1244
tgriesser May 4, 2021
d1b31bf
Adding guards for anchor elements as well as forms
tgriesser May 7, 2021
5af4776
Add tests for _blank
tgriesser May 7, 2021
09abfde
Merge branch 'develop' into tgriesser/fix/1244-redirect-bug
tgriesser May 7, 2021
aac40ed
Capture phase needs to be taken into account for removeEventListener
tgriesser May 7, 2021
43db8be
Add more tests, only guard against _top / _parent
tgriesser May 7, 2021
3963773
Merge branch 'develop' into tgriesser/fix/1244-redirect-bug
tgriesser May 10, 2021
23e0241
Explicitly test to ensure _parent is not stripped in nested frames
tgriesser May 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions packages/driver/cypress/fixtures/issue-1244.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script>
let setCounter = 0
let getCounter = 0
const { getAttribute, setAttribute } = HTMLElement.prototype
HTMLElement.prototype.getAttribute = function() {
if (this.tagName === 'FORM' || this.tagName === 'A') {
getCounter++
}
return getAttribute.apply(this, arguments)
}
HTMLElement.prototype.setAttribute = function(name) {
if (this.tagName === 'FORM' || this.tagName === 'A' && !name.startsWith('data-cypress')) {
setCounter++
}
return setAttribute.apply(this, arguments)
}
function handleEventTarget(e) {
if (e.target.target !== '_top') {
e.target.target = '_top'
}
if (e.target.target !== '_top') {
throw new Error('target property must be top')
}
}
function handleEventAttribute(e) {
if (e.target.getAttribute('target') !== '_top') {
e.target.setAttribute('target', '_top')
}
if (e.target.getAttribute('target') !== '_top') {
throw new Error('Get Attribute must have top')
}
}
window.getCounters = () => {
return {
getCounter,
setCounter
}
}
</script>
<div>
<form method="GET" action="/fixtures/dom.html" onsubmit="handleEventTarget(event)">
<button class="setTarget" type="submit">Submit setTarget</button>
</form>
<form method="GET" action="/fixtures/dom.html" onsubmit="handleEventAttribute(event)">
<button class="setAttr" type="submit">Submit setAttribute</button>
</form>
<form target="_top" method="GET" action="/fixtures/dom.html">
<button class="inline_top" type="submit">Submit inline top</button>
</form>
<form target="_parent" method="GET" action="/fixtures/dom.html">
<button class="inline_parent" type="submit">Submit inline parent</button>
</form>
<form target="_self" method="GET" action="/fixtures/dom.html">
<button class="inline_self" type="submit">Submit inline self</button>
</form>
<form target="_blank" method="GET" action="/fixtures/dom.html">
<button class="inline_blank" type="submit">Submit blank</button>
</form>
<form target="invalid" method="GET" action="/fixtures/dom.html">
<button class="inline_invalid" type="submit">Submit invalid</button>
</form>
<div>
<a class="setTarget" href="/fixtures/dom.html" onclick="handleEventTarget(event)">Link setTarget</a>
<a class="setAttr" href="/fixtures/dom.html" onclick="handleEventAttribute(event)">Link setAttribute</a>
<a class="inline_top" href="/fixtures/dom.html" target="_top">Link top</a>
<a class="inline_parent" href="/fixtures/dom.html" target="_parent">Link parent</a>
<a class="inline_self" href="/fixtures/dom.html" target="_self">Link self</a>
<a class="inline_blank" href="/fixtures/dom.html" target="_blank">Link _blank</a>
<a class="inline_invalid" href="/fixtures/dom.html" target="invalid">Link invalid</a>
</div>
</div>
56 changes: 56 additions & 0 deletions packages/driver/cypress/integration/issues/1244_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
describe('issue 1244', () => {
beforeEach(() => {
cy.visit('/fixtures/issue-1244.html').then(() => {
cy.on('window:before:unload', (e) => {
const win = cy.state('window')

expect(win.getCounters()).to.deep.equal({ getCounter: 0, setCounter: 0 })
})
})
})

for (const [el, target, action] of [['button', 'form', 'submit'], ['a', 'a', 'click']]) {
// <form> submit, <a> click
describe(`<${target}> ${action}`, () => {
it('correctly redirects when target=_top with target.target =', () => {
cy.get(`${el}.setTarget`).click()
cy.get('#dom').should('contain', 'DOM')
cy.url().should('include', 'dom.html')
})

it('correctly redirects when target=_top with setAttribute', () => {
cy.get(`${el}.setAttr`).click()
cy.get('#dom').should('contain', 'DOM')
cy.url().should('include', 'dom.html')
})

it('correctly redirects when target=_top inline in dom', () => {
cy.get(`${el}.inline_top`).click()
cy.get('#dom').should('contain', 'DOM')
cy.url().should('include', 'dom.html')
})

it('correctly redirects when target=_parent inline in dom', () => {
cy.get(`${el}.inline_parent`).click()
cy.get('#dom').should('contain', 'DOM')
cy.url().should('include', 'dom.html')
})

it('maintains behavior when target=_self', () => {
cy.get(`${el}.inline_self`).click()
cy.get('#dom').should('contain', 'DOM')
cy.url().should('include', 'dom.html')
})

it('maintains behavior when target=_blank,invalid', () => {
cy.get(`${el}.inline_blank`).click().then(() => {
cy.url().should('not.include', 'dom.html')
})

cy.get(`${el}.inline_invalid`).click().then(() => {
cy.url().should('not.include', 'dom.html')
})
})
})
}
})
16 changes: 11 additions & 5 deletions packages/driver/src/cy/listeners.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const _ = require('lodash')
const { handleInvalidEventTarget, handleInvalidAnchorTarget } = require('./top_attr_guards')

const HISTORY_ATTRS = 'pushState replaceState'.split(' ')

Expand All @@ -9,9 +10,9 @@ const removeAllListeners = () => {
listenersAdded = false

for (let e of events) {
const [win, event, cb] = e
const [win, event, cb, capture] = e

win.removeEventListener(event, cb)
win.removeEventListener(event, cb, capture)
}

// reset all the events
Expand All @@ -20,10 +21,10 @@ const removeAllListeners = () => {
return null
}

const addListener = (win, event, fn) => {
events.push([win, event, fn])
const addListener = (win, event, fn, capture) => {
events.push([win, event, fn, capture])

win.addEventListener(event, fn)
win.addEventListener(event, fn, capture)
}

const eventHasReturnValue = (e) => {
Expand Down Expand Up @@ -101,6 +102,11 @@ module.exports = {
return callbacks.onSubmit(e)
})

// Handling the situation where "_top" is set on the <form> / <a> element, either in
// html or dynamically, by tapping in at the capture phase of the events
addListener(contentWindow, 'submit', handleInvalidEventTarget, true)
addListener(contentWindow, 'click', handleInvalidAnchorTarget, true)

contentWindow.alert = callbacks.onAlert
contentWindow.confirm = callbacks.onConfirm
},
Expand Down
61 changes: 61 additions & 0 deletions packages/driver/src/cy/top_attr_guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as $elements from '../dom/elements'

const invalidTargets = new Set(['_parent', '_top'])

/**
* Guard against target beting set to something other than blank or self, while trying
* to preserve the appearance of having the correct target value.
*/
export function handleInvalidEventTarget (e: Event & {target: HTMLFormElement | HTMLAnchorElement}) {
let targetValue = e.target.target

if (invalidTargets.has(e.target.target)) {
e.target.target = ''
}

const { getAttribute, setAttribute } = e.target
const targetDescriptor = Object.getOwnPropertyDescriptor(e.target, 'target')

e.target.getAttribute = function (k) {
if (k === 'target') {
return targetValue
}

return getAttribute.call(this, k)
}

e.target.setAttribute = function (k, v) {
if (k === 'target') {
targetValue = v

return $elements.callNativeMethod(this, 'setAttribute', 'cyTarget', v)
}

return setAttribute.call(this, k, v)
}

if (!targetDescriptor) {
Object.defineProperty(e.target, 'target', {
configurable: false,
set (value) {
return targetValue = value
},
get () {
return targetValue
},
})
}
}

/**
* We need to listen to all click events on the window, but only handle anchor elements,
* as those might be the ones where we have an incorrect "target" attr, or could have one
* dynamically set in subsequent event bubbling.
*
* @param e
*/
export function handleInvalidAnchorTarget (e: Event & {target: HTMLAnchorElement}) {
if (e.target.tagName === 'A') {
handleInvalidEventTarget(e)
}
}
1 change: 1 addition & 0 deletions packages/driver/src/dom/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ const nativeMethods = {
addRange: window.Selection.prototype.addRange,
execCommand: window.document.execCommand,
getAttribute: window.Element.prototype.getAttribute,
setAttribute: window.Element.prototype.setAttribute,
setSelectionRange: _nativeSetSelectionRange,
modify: window.Selection.prototype.modify,
focus: _nativeFocus,
Expand Down