How to test your library for vue 2 and vue-next
Tags: vue, vue3, testing
Published: 2020-07-22 14:26:18

With [vue-next][vue-next] now a [release candidate][vue-nextrc], many library authors have begun porting their [vue 2.x][vue] library to [vue-next][vue-next].

There are solutions to enable library compatibility with both Vue 2 and Vue 3. For example, [vue-demi][vue-demi] allows your library to access the appropriate Vue APIs for the version of `vue` you have installed installed.

However, there are no current solutions for _testing_ your library against both Vue 2 and Vue 3. This is particularly relevant to me as I maintain [vue-composable][vue-composable], a library that has both [vue 2.x][vue] and [vue-next][vue-next] support -- and of course, a suite of automated tests.

# Preface

First, an important note: this may not be the most correct or the cleanest approach, but it works and has been tested for quite a few months now.

Probably the best place for this to be used is with the composition-api libraries, because the render has breaking changes, that make this method to be a bit messy.

# Introduction

I will cover how to setup a project to work with both [vue-next][vue-next] and [vue 2 + composition-api][composition-api]. It will target [vue-next][vue-next] primarily.

I've tried to keep the example as simple as possible, but some prior knowledge is required:

- `Vue 3`
- `Vue 2.x` + `@vue/composition-api`
- `Jest`

# Project

For the project we will create a `composable` called [useApi][use-api]

```js
import { ref } from "vue";

export function useApi(factory, handleResponse) {
  const isLoading = ref(false);
  const result = ref(null);
  const error = ref(null);
  const execute = async (...args) => {
    const request = factory(...args);

    isLoading.value = true;
    error.value = null;
    try {
      const response = await fetch(request);
      const valueResponse = await handleResponse(response);

      result.value = valueResponse;
      return valueResponse;
    } catch (e) {
      error.value = e;
      result.value = null;
    } finally {
      isLoading.value = false;
    }
  };

  return {
    isLoading,
    result,
    error,
    execute,
  };
}
```

As written, this would work with [vue-next][vue-next], but not with [vue 2.x][vue].

# api.js

So we will need to replace the `import { ref } from 'vue'` with our own `import { ref } from './api'`.

```js
// api.js [Vue3]
export { ref } from "vue";
```

In `api.js` we will expose all the required Vue APIs needed in the project. **All** `vue` dependencies should come from this file. (Otherwise we might end up importing different `vue` versions and breaking `reactivity`.)

### vue 2 + composition-api

Now we will create a new file called `api.2.js`

```js
export { ref } from "@vue/composition-api";
```

We can hot-swap in this file when testing.

# Test

Now we just need to create a test `__tests__/index.spec.js`

```js
import { useApi } from "../index";
import { mockFetch } from "./utils";

describe("useApi", () => {
  it("should work", async () => {
    mockFetch({ name: "Luke Skywalker" });
    const userList = useApi(
      (page) => ({ url: `https://swapi.dev/api/people/${page}` }),
      (r) => r.json()
    );
    await userList.execute(1);

    expect(userList.result.value.name).toBe("Luke Skywalker");
  });
});
```

Now we could just change the `import { ref } from './api'` to `import { ref } from './api.2'`. Only one problem remains: [@vue/composition-api][composition-api] needs to be initialised by running `Vue.use(VueCompositionApi)`.

# Jest config

So we will add a `jest.config.js` and create a file (`__tests__/setupTest.js`) where we can initialise the Vue 2 `composition-api` plugin.

Because we currently only need the `setupTest.js` when using [vue 2.x][vue] we will pass an environment variable named `VUE` with the version number. If is `2` we will include

```js
// jest.config.js
const isVue2 = process.env.VUE === "2";

module.exports = {
  setupFiles: isVue2 ? ["<rootDir>/__tests__/setupTest.2.js"] : undefined,
  testMatch: ["<rootDir>/__tests__/**/*spec.[jt]s?(x)"],
};
```

For the `setupTest.2.js` we will need to import `vue`, but we can't just import it normally because we already have `vue@next` in our package dependencies. So we will need to add `vue 2.x` with a different name (`vue2`).

```json
// package.json
  "dependencies": {
    "@vue/composition-api": "^1.0.0-beta.6",
    "vue": "^3.0.0-rc.4",
    "vue2": "npm:vue@^2.6.1"
  }
```

Now we can import it in `setupTest.2.js`

```js
import Vue from "vue2";
import VueCompositionApi from "@vue/composition-api";

Vue.use(VueCompositionApi);
```

Great. However, we're not done yet. If you pass `env.VUE === '2'`, it may initialize the composition api, but the tests would still run using [vue-next][vue-next] 🤔

To make sure we run with the correct API we will mock the `api.js` module with jest.

```js
// setupTest.2.js

// mock for the correct api
jest.mock("./../api", () => jest.requireActual("./../api.2"));

//... importing vue and use CompositionApi
```

Now if you pass the `VUE=2` environemnt variable it will use [@vue/composition-api][composition-api] instead of [vue-next][vue-next].

## Scripts

You can have `yarn test` test both versions by doing

```json
// package.json
  "scripts": {
    "test": "yarn test:vue2 && yarn test:vue3",
    "test:vue2": "VUE=2 yarn jest",
    "test:vue3": "yarn jest"
  },
```

### Multi-platform

You can install `cross-env` and run both

```bash
yarn add cross-env -D
```

```json
//package.json
  "scripts": {
    "test": "yarn test:vue2 && yarn test:vue3",
    "test:vue2": "cross-env VUE=2 yarn jest",
    "test:vue3": "yarn jest"
  },
```

# Pitfalls

If there's file that imports `vue` or `@vue/composition-api`, most likely it will compile but it will not work properly, reactivity will not work, etc. Please make sure you only import from `api.js`.

`Typescript` types might be different and have some nuances, if using with `typescript` please export the types from `api.ts`, if the types don't exist in `v2` or `v3`, export custom types using `api.ts` or `api.2.ts`.

# Conclusion

Hopefully this will give you a good start to begin experimenting with both libraries within the same code base. There are a few missing apis in [@vue/composition-api][composition-api], and this approach will allow you to implement some of them ([example](https://github.com/pikax/vue-composable/blob/24de670498a5f9353e837251d6f7ad98c8e6e174/packages/vue-composable/src/api.2.ts#L31-L33))

Please check up-to-date [repo][boilerplate] for the all the code and working project.


Big thanks to [@danielroe](https://github.com/danielroe) for reviewing this article, so many awesome changes 🎉

[vue]: https://github.com/vuejs/vue
[vue-next]: https://github.com/vuejs/vue-next
[vue-nextrc]: https://github.com/vuejs/rfcs/issues/189
[vue-demi]: https://github.com/antfu/vue-demi
[vue-composable]: https://github.com/pikax/vue-composable
[use-api]: https://vuedose.tips/use-composition-api-to-easily-handle-api-requests-in-vue-js/
[composition-api]: https://github.com/vuejs/composition-api
[boilerplate]: https://github.com/pikax/vue-test-2-next-boilerplate