Stub The Form That Opens The Second Browser Tab

How Cypress can prevent a form from popping a second browser window.

A user complained that a small Cypress test opens a new browser window and Cypress cannot continue with its test.

1
2
3
4
it('opens 2nd browser window', () => {
cy.visit('public/index.html')
cy.contains('a', 'Open').click()
})

The test opens the second browser window

The user has provided a reproducible example, so great job! I have marked the important steps in the code that we will stub from our Cypress spec file.

The application code

Let's prevent the application from opening the second browser widow.

🎁 You can find the source code and the spec files in the repo bahmutov/cypress-form-opens-second-tab-example.

Remove the onclick attribute

The simplest solution to prevent the application code from running is to remove the onclick attribute marked with "1" in the code screenshot above. The application is using this attribute to call the JavaScript function, so by removing it we avoid opening the second browser window.

cypress/e2e/1-remove-onlick.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
it('removes the onclick attribute', () => {
cy.visit('public/index.html')

// the application opens the 2nd tab in response
// to the click handler set via "onclick=..." attribute
cy.contains('a', 'Open')
// confirm the A element has the "onclick" attribute
.should('have.attr', 'onclick')
// disable the behavior by removing the "onclick" attribute
cy.contains('a', 'Open').invoke('attr', 'onclick', '').click()
// confirm we remain on the home screen
cy.location('pathname').should('include', 'index.html')
})

The test stops the code execution by removing the onclick attribute

The test simply does nothing - it does not execute any of the application's code, which is probably not what we want. We want the test to run as much code as possible! The shown approach also would not work if the application attached the click event listener using addEventListener instead of using the element's attribute.

My rating of the above solution 1 out of 10 stars.

Stub the internal application code

The click calls the application code that calls another application function by using the window.openNew call. This is marked "2" in the code screenshot above:

app.js
1
2
3
4
5
6
function openNewTab() {
window.openNew({
target: 'test_blank',
url: 'submitted.html',
})
}

By calling the "openNew" as a window's property, the application allows Cypress to easily spy / stub the application's call.

cypress/e2e/2-stub-openNew.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('passes', () => {
cy.visit('public/index.html')

// the application internally calls
// the "window.openNew" method
// we can stub it using the cy.stub command
cy.window().then((win) => {
cy.stub(win, 'openNew').as('openNew')
})
cy.contains('a', 'Open').click()
// and confirm the stub was called as expected
cy.get('@openNew')
.should('have.been.calledOnce')
.its('firstCall.args.0')
.should('deep.equal', {
target: 'test_blank',
url: 'submitted.html',
})
})

The test confirms the application calls the internal code correctly after a click

Using this approach we can confirm the user interface and the application code are working correctly. We can let the app's code run all the way to the "edge" and stub the low-level method use to make the final call that opens the second browser window. Thus I rate this powerful approach 6 out of 10 stars.

Tip: see more cy.stub and related code examples at my Stubs, spies, and clocks examples page.

Stub the form submit method

In the previous test, we have stubbed the application's method window.openNew. We can also stub pretty much any browser API. For example, our application is preparing a form to submit. Then it calls the form.submit() method, marked "3" in the code snapshot:

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function openNew(option) {
const form = document.createElement('form')
form.target = option.target || '_blank'
form.action = option.url
form.method = 'GET'

// send some additional information
const name = document.createElement('input')
name.setAttribute('type', 'text')
name.setAttribute('name', 'firstName')
name.setAttribute('value', 'Joe')
form.appendChild(name)

document.body.appendChild(form)
form.submit() // 3
$(form).remove()
}

The problem is the last call form.submit() - it submits the form to the server. We are interested in the form preparation, but don't want it to actually be submitted. Thus let's stub the form.submit method.

cypress/e2e/3-stub-form-submit.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
it('passes', () => {
cy.visit('public/index.html')

// the application will create the form
// and call its submit method. Let's
// stub the form.submit() to prevent
cy.document().then((doc) => {
const create = doc.createElement.bind(doc)
const stub = cy.stub(doc, 'createElement')

// all calls should still go to the original method
stub.callThrough()
// if the app is calling document.createElement("form")
// then call our own method that created the form element
// but stubs its "submit()" method
stub.withArgs('form').callsFake(() => {
const form = create('form')
cy.stub(form, 'submit').as('submit')
return form
})
})
cy.contains('a', 'Open').click()
cy.get('@submit').should('have.been.calledOnce')
// you could confirm the form's attributes and input elements
// by getting them from the "@submit" stub instance
})

The above test has a lot of comments explaining what we are trying to do: we want to keep using the document.createElement method, but if the application's code calls document.createElement('form') then we return the form instance with the stubbed submit method. Thus the application's code does everything, only the final browser submit call goes to our stub instance.

The test stubs the final form submit method

Cypress tests run in the browser, thus the same cy.spy and cy.stub methods work on the browser's own APIs like document.createElement. By letting the application code run all the way to the last form.submit() the test is almost end-to-end. Thus I rate this test 8 out of 10 stars.

Stub the form target property

Why does our form open the second browser window? Because the form has the target: test_blank property, marked in the code snapshot with "4".

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
function openNewTab() {
window.openNew({
target: 'test_blank',
url: 'submitted.html',
})
}

function openNew(option) {
const form = document.createElement('form')
form.target = option.target || '_blank' // "4"
form.action = option.url
...
}

Just like <a target="..."> elements, the form can be loaded in the current browser tab by using the target: _self value. Let's adjust our spec to stub the form's target property and set it always to _self.

cypress/e2e/4-stub-form-target.cy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
it('passes', () => {
cy.visit('public/index.html')

// before clicking on the link, stub the Document.createElement
// and if the user is trying to create a new form, stub its
// property "target" to not allow opening new tabs; always have it at "_self"
cy.document().then((doc) => {
const create = doc.createElement.bind(doc)
cy.stub(doc, 'createElement').callsFake((name) => {
if (name === 'form') {
const form = create('form')
cy.stub(form, 'target').value('_self')
// Also spy on the instance method "submit"
// so that later we can validate the submitted form
cy.spy(form, 'submit').as('submit')
return form
} else {
return create(name)
}
})
})

// spy on the network call to submit the form
cy.intercept({
method: 'GET',
pathname: 'submitted.html',
}).as('submitted')

// click on the link and confirm the form
// has loaded in the current tab
cy.contains('a', 'Open').click()
cy.contains('Thank you')

// verify the network call to submit the form
// has the expected URL search parameters
cy.wait('@submitted')
.its('request.url')
.then((url) => {
const parsed = new URL(url)
return parsed.searchParams
})
// the form submits the field "firstName=Joe"
.invoke('get', 'firstName')
.should('equal', 'Joe')

// grab the form's submit call
// to get back to the form and its input elements
cy.get('@submit')
.should('have.been.calledOnce')
.its('firstCall.thisValue.elements')
// the form's HTML elements include every input element
.then((elements) => {
// we can validate the form's first name input element
// has the expected value set by the application' code
expect(elements.namedItem('firstName'), 'first name').to.have.value('Joe')
})
})

The form is loaded in the same browser tab as the current page, and we can continue working with the form, as if we switched to the second tab.

The test stubs the final form submit method

The above test has many things going for it:

  • it lets the form be submitted to the backend, and it verifies the submitted URL parameters by using cy.intercept command and the browser's own URL API.
  • it verifies the form element and its input elements by getting them from the submit spy

I rate the above test 9 out of 10 stars.

If only Cypress could control two tabs, then the test would earn 10 ⭐️.

See also