Let's say the application changes the URL using the assignment:
1 | location.href = 'https://acme.com' |
Can we prevent the URL change? For example, we might want to limit our test to the current origin. Let's do it using the following example page:
1 | <body> |
1 | setTimeout(() => { |
The initial Cypress test does not prevent the navigation to "acme.com"
1 | it('sets the location HREF', () => { |
🎁 The source code for this blog post is located in the repo bahmutov/with-window.
We need to prevent location.href = ...
assignment by making the property read-only or better: using a custom object property definition with a setter stub function. Unfortunately, we cannot overwrite the location.href
property:
Ok, maybe we can mock the entire location
object? It is a property of the global window
object:
Unfortunately, the location
property itself cannot be overwritten either.
We need another way. In the blog post Stub The Unstubbable I have shown one possible solution that modifies the application's source code to create an intermediate proxy "Location" instance. The E2E test can control that object. It is imperfect solution, as it requires source code modifications, which might be unavailable.
Let's find another way.
JavaScript with
keyword
JavaScript has a pretty obscure operator with that you should definitely NOT use in production, but can help us with our testing needs.
1 | with (expression) { |
The above syntax adds the expression
result into the variable lookup chain. For example:
1 | let a, x, y; |
In the example above, the PI
, cos
, and sin
are properties of the Math
object. By using with (Math)
we are forcing the browser to look up these identifiers in the Math
object (before going up to the window
object).
Tip: the with (expression)
syntax is hard to read and understand. A simple spread operator would be much more preferable way of coding the above example:
1 | let a, x, y; |
The E2E test
The application's code accessing the location
object is loaded by the <script src="app.js"></script>
resource. Let's wrap this code using a fake window
object just so we can "sneak" in a fake "location" object of our creation.
1 | it('sets the location HREF', () => { |
When the test runs, the application loads app.js
and receives the following (modified) script
1 | const fakeWindowObject = { |
The test stays on the same page, but we do see the application printing "changing window location to acme.com".
We need to verify the location.href
property really changes to acme.com
string. We can put the fake window object on the real window object. Then we can get its value using the cy.window command.
One last tip: the browser caches the app.js
resource, thus we need to remove the caching headers in order to receive the full JavaScript source code:
1 | cy.intercept('GET', 'app.js', (req) => { |
Proxy to the real location
We don't want to create a completely fake location
object, since we want to be able to use the real properties and methods in Location.prototype
. Instead of plain object, our fake location can proxy to the real thing:
1 | it('sets the location HREF', () => { |
To see the proxy in action, I will modify the console.log
message the application prints:
1 | setTimeout(() => { |
The test runs and we see the real host name, yet href = ...
assignment is trapped.
Nice.