DEV Community

Cover image for How to test "window.open" in Vue with Vitest
Leonardo Salvucci
Leonardo Salvucci

Posted on

How to test "window.open" in Vue with Vitest

As we gain experience as a developer and requirements become more complex, testing provides us with a powerful tool to take firm steps forward. It also gives us security when we can verify that all tests still pass after making changes in our code.

In this post we are going to carry out a simple test as an exercise to review some basic concepts and, through a practical example, understand how to take advantage of testing utilities in case that our component requires opening a new tab using window.open function.

You can find the code in this repository --> github

Disclaimer: This post is not about TDD. I just try to explain step by step what happens when someone tries to learn new concepts from practice and also show you the way i usually do it

Setting up our project

Let's start creating a new project with vite. We will name it vue-mocking-window-object.



yarn create vite


Enter fullscreen mode Exit fullscreen mode

Select Vue and Typescript

Image description

Image description

Vite doesn't install dependencies so we will have to do it.



cd vue-mocking-window-object
yarn install


Enter fullscreen mode Exit fullscreen mode

Now we have to add vitest, @vue/test-utils and typescript types to be used in our project.



yarn add -D vitest @vue/test-utils @types/jest


Enter fullscreen mode Exit fullscreen mode

Note: -D is for installing libraries only as a development dependency

The next step is to add scripts to package.json so we can use them with yarn. Just edit package.json and add this



"scripts": {
    "test": "vitest",
    "coverage": "vitest run --coverage"
}


Enter fullscreen mode Exit fullscreen mode

We have to modify vite's config to add testing configuration.



import { defineConfig, UserConfigExport } from 'vitest/config'
import vue from '@vitejs/plugin-vue'


const config: UserConfigExport =
{
 plugins: [vue()],
 test: {
   globals: true,
   environment: 'jsdom',
 }
}
export default defineConfig(config)


Enter fullscreen mode Exit fullscreen mode

By setting globals as true, it will inject jest into tests without needing to importing it.
environment is being used for configuring vitest to set a browser-like env for our tests, otherwise it will be a node.js env and some global object like document won't be available in that case.

And that's all! Let's create a simple test to verify if everything is working as expected.

Creating our first test

Modify the file App.vue (see below) and delete file HelloComponent.vue from components folder.



<template>
  <div>Welcome to Vue + Vitest</div>
</template>


Enter fullscreen mode Exit fullscreen mode

Then, if we start development server with yarn dev and open a browser with url http://localhost:5173 we should get this page.

Image description

Now create a file named App.spec.ts in same folder as App.vue and write the following code:



import { mount } from '@vue/test-utils';
import App from './App.vue';

describe('Testing APP component', () => {
  it('render component', async () => {
    const wrapper = mount(App);

    expect(wrapper.text()).toContain('Welcome to Vue + Vitest');
  })
})


Enter fullscreen mode Exit fullscreen mode

Run in a terminal yarn test and...

Image description

Yes!!! That's was easy right?
With help of vue test-utils we can render the component into a variable and use jest to create our validations. In this test we can verify that the string 'Welcome to Vue + Vitest' is being rendered by the component.

Ok, let's try something a little bit more complex.

Create base component

We want a component that renders an ethereum wallet address and by clicking on it let us open a new tab that shows the etherscan of that wallet. Etherscan.io is a public site where you can search and trace Ethereum transactions just like transfer, contracts interaction, etc...

For doing this (and just keeping this post simple) create a file named EthereumAddress.vue in components folder and write this code:




<script setup lang="ts">
type Props = {
 address: string;
};
defineProps<Props>();
</script>

<template>
 <div class="ethereum-address">{{ address }}</div>
</template>

<style>
.ethereum-address {
 font-family: "Courier New";
}
</style>


Enter fullscreen mode Exit fullscreen mode

Edit App.vue to import and mount this component and check what happens in browser



<script setup lang="ts">
import EthereumAddress from "./components/EthereumAddress.vue";
</script>

<template>
  <div>Welcome to Vue + Vitest</div>
  <EthereumAddress address="0x839Be166ee5Ac99309C0A0796867942822a5E8BE" />
</template>


Enter fullscreen mode Exit fullscreen mode

And now we see this

Image description

Ok, great, it works. It's good time to create a test for our component.

Create a file named EthereumAddress.spec.ts and write this code:



import { mount } from '@vue/test-utils';
import EthereumAddress from './EthereumAddress.vue';

// Testing purpose ethereum address
const testingAddress = '0x839Be166ee5Ac99309C0A0796867942822a5E8BE';

describe('EthereumAddress Component', () => {
  it('render address passed as prop', async () => {
    // Render component
    const wrapper = mount(EthereumAddress, {
      props: {
        address: testingAddress
      }
    });

    // Verify
    expect(wrapper.text()).toBe(testingAddress);
  })
})


Enter fullscreen mode Exit fullscreen mode

run yarn test again and verify that all tests passed
Image description

 Add functionality

We want to open etherscan when a user clicks on it. For making that happen we will open a new tab by using window.open function.

We need base url of etherscan and, as a good practice, make all the global constants be imported from .env file because probably you will need different base urls for each environment (development, testing, production, etc...)

Create a .env file and write this



VITE_APP_ETHERSCAN_URL=https://etherscan.io/address/


Enter fullscreen mode Exit fullscreen mode

Now create a file named constants.ts to import this var from .env. By using a single file to define all the constants is also a good practice to avoid importing from .env all over the app. It gives you more flexibility when something changes and adds an extra layer of validation like this.



export const ETHERSCAN_URL = process.env.VITE_APP_ETHERSCAN_URL;
if (!ETHERSCAN_URL) {
 throw new Error('VITE_APP_ETHERSCAN_URL must be set on .env');
};


Enter fullscreen mode Exit fullscreen mode

Modify EthereumAddress.vue file and write this code:



<script setup lang="ts">
import { ETHERSCAN_URL } from "../constants";
type Props = {
  address: string;
};

const props = defineProps<Props>();

const handleEtherscan = () => {
  window.open(`${ETHERSCAN_URL}${props.address}`, "_blank");
};
</script>

<template>
  <div
    class="ethereum-address"
    @click="handleEtherscan"
    data-testid="ethereumAddress"
  >
    {{ address }}
  </div>
</template>

<style>
.ethereum-address {
  font-family: "Courier New";
  cursor: pointer;
}
</style>


Enter fullscreen mode Exit fullscreen mode

Notice that we add data-testid property in div component, it makes easy to "find" the component on our tests

Let's modify EthereumAddress.spec.ts by adding a test which makes a click on the component and see what happens



import { mount } from '@vue/test-utils';
import EthereumAddress from './EthereumAddress.vue';

// Testing purpose ethereum address
const testingAddress = '0x839Be166ee5Ac99309C0A0796867942822a5E8BE';
// testid reference tag
const referenceLabel = '[data-testid="ethereumAddress"]';

describe('EthereumAddress Component', () => {
  it('render address passed as prop', async () => {
    // Render component
    const wrapper = mount(EthereumAddress, {
      props: {
        address: testingAddress
      }
    });

    // Verify
    expect(wrapper.text()).toBe(testingAddress);
  })

  it('click address and verify if a new tab was opened', async () => {
   const wrapper = mount(EthereumAddress, {
     props: {
       address: testingAddress
     }
   });

   wrapper.find(referenceLabel).trigger('click')
  })

})


Enter fullscreen mode Exit fullscreen mode

Probably you must see something like this...

Image description

What this means is that in the testing environment window.open doesn't exists, because it says it's not implemented.
Okay, now what?
We just want to know if window.open was called with etherscan url and _blank as parameters and we don't care what this function does in the browser. What we are going to do is to use a tool called spy to verify if the function was called or not. Vitest provides this functionality!
Modify the test like this



import { mount } from '@vue/test-utils';
import { vi } from 'vitest';
import EthereumAddress from './EthereumAddress.vue';

// Testing purpose ethereum address
const testingAddress = '0x839Be166ee5Ac99309C0A0796867942822a5E8BE';
// testid reference tag
const referenceLabel = '[data-testid="ethereumAddress"]';
// Etherscan url from environmend
const ETHERSCAN_URL = import.meta.env.VITE_APP_ETHERSCAN_URL;

describe('EthereumAddress Component', () => {
  it('render address passed as prop', async () => {
    // Render component
    const wrapper = mount(EthereumAddress, {
      props: {
        address: testingAddress
      }
    });

    // Verify
    expect(wrapper.text()).toBe(testingAddress);
  })

  it('click address and verify if a new tab was opened', async () => {
    // Render component
    const wrapper = mount(EthereumAddress, {
      props: {
        address: testingAddress
      }
    });
    // Create spy for testing if window.open was called
    const spy = vi.spyOn(window, 'open');

    // Act: click on the address
    wrapper.find(referenceLabel).trigger('click')

    // Verify if window.open was called
    expect(spy).toBeCalledWith(`${ETHERSCAN_URL}${testingAddress}`, '_blank');
  })
})


Enter fullscreen mode Exit fullscreen mode

Run tests! now must be ok!

Image description

Well, our tests pass but what is that!?!?
window.open still throw error... ummm

What happened here was that the function window.open was correctly called, with correct parameters but the function actually doesn't exists!

We can use another testing tool to create a global function named window.open to avoid this error. It's named stub. A stub is a piece of code used to substitute another functionality just for testing purposes and vitest provides us with this tool.

Add this line of code at the beginning of test after describe



// Mock global window.open
vi.stubGlobal('open', vi.fn());


Enter fullscreen mode Exit fullscreen mode

And now everything is fine, our tests passed and no errors were shown. Congratulations!

Top comments (0)