How to test "window.open" in Vue with Vitest
Tags: vue, vite, testing, beginners
Published: 2022-09-22 22:16:10

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](https://github.com/LeonardoSalvucci/vue-mocking-window-object.git)

> 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_.

```bash
yarn create vite
```

Select Vue and Typescript

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/suz3fbjdqb1j9u6bl49s.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bdm1jpszu61cb6xuoeow.png)

Vite doesn't install dependencies so we will have to do it.
```
cd vue-mocking-window-object
yarn install
```

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
```
_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
```json
"scripts": {
    "test": "vitest",
    "coverage": "vitest run --coverage"
}
```

We have to modify vite's config to add testing configuration.
```typescript
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)
```
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.
```typescript
<template>
  <div>Welcome to Vue + Vitest</div>
</template>
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v6qnddu5a702dvsylhyj.png)

Now create a file named App.spec.ts in same folder as App.vue and write the following code:
```typescript
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');
  })
})
```
Run in a terminal `yarn test` and...

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p7tm7xvwzrqw6p8j5o03.png)

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:
```typescript

<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>
```

Edit App.vue to import and mount this component and check what happens in browser
```typescript
<script setup lang="ts">
import EthereumAddress from "./components/EthereumAddress.vue";
</script>

<template>
  <div>Welcome to Vue + Vitest</div>
  <EthereumAddress address="0x839Be166ee5Ac99309C0A0796867942822a5E8BE" />
</template>
```
And now we see this

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mib0h575neoqu247aet.png)

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:
```typescript
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);
  })
})
```
run `yarn test` again and verify that all tests passed
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eyslzhgf0mq8eb8bxn0d.png)

### 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
```javascript
VITE_APP_ETHERSCAN_URL=https://etherscan.io/address/
```
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.
```typescript
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');
};
```
Modify EthereumAddress.vue file and write this code:
```typescript
<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>
```
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
```typescript
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')
  })

})
```
Probably you must see something like this...

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/40dvcavatz0jan97vq45.png)

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
```typescript
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');
  })
})
```
Run tests! now must be ok!

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u8fu5jpdodbqsp8vwdzs.png)

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`
```typescript
// Mock global window.open
vi.stubGlobal('open', vi.fn());
```
And now everything is fine, our tests passed and no errors were shown. Congratulations!